mirror of
https://github.com/hknsh/project-knedita.git
synced 2024-11-28 17:41:15 +00:00
feat: replaced prisma to kysely for queries
This commit is contained in:
parent
bc40355fa2
commit
86b9f6ead8
20 changed files with 712 additions and 352 deletions
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
@ -48,15 +48,19 @@
|
||||||
"dotenv-expand": "^11.0.6",
|
"dotenv-expand": "^11.0.6",
|
||||||
"file-type": "16.5.4",
|
"file-type": "16.5.4",
|
||||||
"ioredis": "^5.4.1",
|
"ioredis": "^5.4.1",
|
||||||
|
"kysely": "^0.27.4",
|
||||||
"nestjs-s3": "^2.0.1",
|
"nestjs-s3": "^2.0.1",
|
||||||
"nestjs-zod": "^3.0.0",
|
"nestjs-zod": "^3.0.0",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
|
"pg": "^8.13.0",
|
||||||
|
"prisma-kysely": "^1.8.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"sharp": "^0.33.5",
|
"sharp": "^0.33.5",
|
||||||
"tstl": "^3.0.0"
|
"tstl": "^3.0.0",
|
||||||
|
"uuid": "^10.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.5.3",
|
"@biomejs/biome": "1.5.3",
|
||||||
|
@ -72,7 +76,9 @@
|
||||||
"@types/node": "^20.16.11",
|
"@types/node": "^20.16.11",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/passport-local": "^1.0.38",
|
"@types/passport-local": "^1.0.38",
|
||||||
|
"@types/pg": "^8.11.10",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"husky": "^9.1.6",
|
"husky": "^9.1.6",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"lint-staged": "^15.2.10",
|
"lint-staged": "^15.2.10",
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- The primary key for the `KweekLike` table will be changed. If it partially fails, the table could be left without primary key constraint.
|
||||||
|
- You are about to drop the column `id` on the `KweekLike` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "KweekLike" DROP CONSTRAINT "KweekLike_pkey",
|
||||||
|
DROP COLUMN "id",
|
||||||
|
ADD CONSTRAINT "KweekLike_pkey" PRIMARY KEY ("kweekId", "userId");
|
|
@ -0,0 +1,11 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- The primary key for the `CommentLike` table will be changed. If it partially fails, the table could be left without primary key constraint.
|
||||||
|
- You are about to drop the column `id` on the `CommentLike` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "CommentLike" DROP CONSTRAINT "CommentLike_pkey",
|
||||||
|
DROP COLUMN "id",
|
||||||
|
ADD CONSTRAINT "CommentLike_pkey" PRIMARY KEY ("commentId", "userId");
|
|
@ -1,5 +1,7 @@
|
||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-kysely"
|
||||||
|
output = "../src/db"
|
||||||
|
fileName = "types.ts"
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
|
@ -39,23 +41,25 @@ model Kweek {
|
||||||
}
|
}
|
||||||
|
|
||||||
model KweekLike {
|
model KweekLike {
|
||||||
id String @id @default(uuid())
|
|
||||||
kweekId String
|
kweekId String
|
||||||
kweek Kweek @relation(fields: [kweekId], references: [id], onDelete: Cascade)
|
kweek Kweek @relation(fields: [kweekId], references: [id], onDelete: Cascade)
|
||||||
userId String
|
userId String
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@id([kweekId, userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
// I should join these two up? Yeah, but I will not do it since it didn't work on the first time.
|
// I should join these two up? Yeah, but I will not do it since it didn't work on the first time.
|
||||||
|
|
||||||
model CommentLike {
|
model CommentLike {
|
||||||
id String @id @default(uuid())
|
|
||||||
commentId String
|
commentId String
|
||||||
comment Comments @relation(fields: [commentId], references: [id], onDelete: Cascade)
|
comment Comments @relation(fields: [commentId], references: [id], onDelete: Cascade)
|
||||||
userId String
|
userId String
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@id([commentId, userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Follows {
|
model Follows {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { FastifyMulterModule } from "@nest-lab/fastify-multer";
|
import { FastifyMulterModule } from "@nest-lab/fastify-multer";
|
||||||
import { ThrottlerStorageRedisService } from "@nest-lab/throttler-storage-redis";
|
import { ThrottlerStorageRedisService } from "@nest-lab/throttler-storage-redis";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { ConfigModule } from "@nestjs/config";
|
import { ConfigModule, ConfigService } from "@nestjs/config";
|
||||||
import { APP_GUARD, APP_PIPE } from "@nestjs/core";
|
import { APP_GUARD, APP_PIPE } from "@nestjs/core";
|
||||||
import { ThrottlerGuard, ThrottlerModule, seconds } from "@nestjs/throttler";
|
import { ThrottlerGuard, ThrottlerModule, seconds } from "@nestjs/throttler";
|
||||||
import { S3Module } from "nestjs-s3";
|
import { S3Module } from "nestjs-s3";
|
||||||
|
@ -10,6 +10,7 @@ import { AuthModule } from "./auth/auth.module";
|
||||||
import { JwtAuthGuard } from "./auth/jwt-auth.guard";
|
import { JwtAuthGuard } from "./auth/jwt-auth.guard";
|
||||||
import { Configuration } from "./configuration";
|
import { Configuration } from "./configuration";
|
||||||
import { KweeksModule } from "./kweeks/kweeks.module";
|
import { KweeksModule } from "./kweeks/kweeks.module";
|
||||||
|
import { KyselyModule } from "./services/kysely/kysely.module";
|
||||||
import { UserModule } from "./users/users.module";
|
import { UserModule } from "./users/users.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
@ -19,6 +20,15 @@ import { UserModule } from "./users/users.module";
|
||||||
storage: new ThrottlerStorageRedisService(Configuration.REDIS_URL()),
|
storage: new ThrottlerStorageRedisService(Configuration.REDIS_URL()),
|
||||||
errorMessage: "Too many requests",
|
errorMessage: "Too many requests",
|
||||||
}),
|
}),
|
||||||
|
KyselyModule.forRootAsync({
|
||||||
|
useFactory: () => ({
|
||||||
|
host: Configuration.POSTGRES_HOST(),
|
||||||
|
port: Number(Configuration.POSTGRES_PORT()),
|
||||||
|
user: Configuration.POSTGRES_USER(),
|
||||||
|
password: Configuration.POSTGRES_PASSWORD(),
|
||||||
|
database: Configuration.POSTGRES_DB(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({
|
||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -11,4 +11,9 @@ export namespace Configuration {
|
||||||
export const MINIO_DEFAULT_BUCKETS = () =>
|
export const MINIO_DEFAULT_BUCKETS = () =>
|
||||||
Environment.env.MINIO_DEFAULT_BUCKETS;
|
Environment.env.MINIO_DEFAULT_BUCKETS;
|
||||||
export const MINIO_ENDPOINT = () => Environment.env.MINIO_ENDPOINT;
|
export const MINIO_ENDPOINT = () => Environment.env.MINIO_ENDPOINT;
|
||||||
|
export const POSTGRES_HOST = () => Environment.env.POSTGRES_HOST;
|
||||||
|
export const POSTGRES_PORT = () => Environment.env.POSTGRES_PORT;
|
||||||
|
export const POSTGRES_USER = () => Environment.env.POSTGRES_USER;
|
||||||
|
export const POSTGRES_PASSWORD = () => Environment.env.POSTGRES_PASSWORD;
|
||||||
|
export const POSTGRES_DB = () => Environment.env.POSTGRES_DB;
|
||||||
}
|
}
|
||||||
|
|
71
src/db/types.ts
Normal file
71
src/db/types.ts
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import type { ColumnType } from "kysely";
|
||||||
|
export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
|
||||||
|
? ColumnType<S, I | undefined, U>
|
||||||
|
: ColumnType<T, T | undefined, T>;
|
||||||
|
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
|
||||||
|
|
||||||
|
export const NotificationType = {
|
||||||
|
WARNING: "WARNING",
|
||||||
|
INFO: "INFO",
|
||||||
|
} as const;
|
||||||
|
export type NotificationType =
|
||||||
|
(typeof NotificationType)[keyof typeof NotificationType];
|
||||||
|
export type CommentLike = {
|
||||||
|
commentId: string;
|
||||||
|
userId: string;
|
||||||
|
createdAt: Generated<Timestamp>;
|
||||||
|
};
|
||||||
|
export type Comments = {
|
||||||
|
id: Generated<string>;
|
||||||
|
content: string;
|
||||||
|
userId: string;
|
||||||
|
kweekId: string | null;
|
||||||
|
attachments: string[];
|
||||||
|
createdAt: Generated<Timestamp>;
|
||||||
|
updatedAt: Generated<Timestamp>;
|
||||||
|
parentId: string | null;
|
||||||
|
};
|
||||||
|
export type Follows = {
|
||||||
|
followerId: string;
|
||||||
|
followingId: string;
|
||||||
|
};
|
||||||
|
export type Kweek = {
|
||||||
|
id: Generated<string>;
|
||||||
|
content: string;
|
||||||
|
authorId: string;
|
||||||
|
attachments: string[];
|
||||||
|
createdAt: Generated<Timestamp>;
|
||||||
|
updatedAt: Timestamp;
|
||||||
|
};
|
||||||
|
export type KweekLike = {
|
||||||
|
kweekId: string;
|
||||||
|
userId: string;
|
||||||
|
createdAt: Generated<Timestamp>;
|
||||||
|
};
|
||||||
|
export type Notifications = {
|
||||||
|
id: Generated<string>;
|
||||||
|
type: NotificationType;
|
||||||
|
content: string;
|
||||||
|
createdAt: Generated<Timestamp>;
|
||||||
|
fromUserId: string;
|
||||||
|
toUserId: string;
|
||||||
|
};
|
||||||
|
export type User = {
|
||||||
|
id: Generated<string>;
|
||||||
|
displayName: string | null;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
profileImage: string | null;
|
||||||
|
socketId: string | null;
|
||||||
|
createdAt: Generated<Timestamp>;
|
||||||
|
};
|
||||||
|
export type DB = {
|
||||||
|
CommentLike: CommentLike;
|
||||||
|
Comments: Comments;
|
||||||
|
Follows: Follows;
|
||||||
|
Kweek: Kweek;
|
||||||
|
KweekLike: KweekLike;
|
||||||
|
Notifications: Notifications;
|
||||||
|
User: User;
|
||||||
|
};
|
|
@ -7,11 +7,15 @@ import {
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { PrismaService } from "src/services/prisma/prisma.service";
|
import { PrismaService } from "src/services/prisma/prisma.service";
|
||||||
import { S3Service } from "src/services/s3/s3.service";
|
import { S3Service } from "src/services/s3/s3.service";
|
||||||
|
import { CommentsRepository } from "./repository/comments.repository";
|
||||||
|
import { KweeksRepository } from "./repository/kweeks.repository";
|
||||||
import { selectCommentsWithReplies } from "./schemas/prisma_queries.schema";
|
import { selectCommentsWithReplies } from "./schemas/prisma_queries.schema";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CommentsService {
|
export class CommentsService {
|
||||||
constructor(
|
constructor(
|
||||||
|
private readonly commentsRepository: CommentsRepository,
|
||||||
|
private readonly kweeksRepository: KweeksRepository,
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly s3: S3Service,
|
private readonly s3: S3Service,
|
||||||
) {}
|
) {}
|
||||||
|
@ -29,33 +33,24 @@ export class CommentsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verifies if the kweek_id is a kweek or a comment
|
// Verifies if the kweek_id is a kweek or a comment
|
||||||
const parentComment = await this.prisma.comments.findUnique({
|
const parentComment = await this.commentsRepository.findOne(kweek_id);
|
||||||
where: { id: kweek_id },
|
|
||||||
});
|
|
||||||
|
|
||||||
let kweek = null;
|
let kweek = undefined;
|
||||||
|
|
||||||
if (parentComment === null) {
|
if (parentComment === undefined) {
|
||||||
kweek = await this.prisma.kweek.findFirst({
|
kweek = await this.kweeksRepository.findOne(kweek_id);
|
||||||
where: { id: kweek_id },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (kweek === null) {
|
if (kweek === undefined) {
|
||||||
throw new NotFoundException("Kweek/Comment not found");
|
throw new NotFoundException("Kweek/Comment not found");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id } = await this.prisma.comments.create({
|
const { id } = await this.commentsRepository.create(
|
||||||
data: {
|
content,
|
||||||
content,
|
user_id,
|
||||||
userId: user_id,
|
kweek ? kweek.id : null,
|
||||||
kweekId: kweek ? kweek.id : null,
|
parentComment ? parentComment.id : null,
|
||||||
parentId: parentComment ? parentComment.id : null,
|
);
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
let attachments = [];
|
let attachments = [];
|
||||||
|
|
||||||
|
@ -63,41 +58,34 @@ export class CommentsService {
|
||||||
attachments = await this.s3.multiImageUpload(id, files);
|
attachments = await this.s3.multiImageUpload(id, files);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.prisma.comments.update({
|
await this.commentsRepository.addAttachments(id, attachments);
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
attachments,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return await this.prisma.comments.findFirst({ where: { id } });
|
return await this.commentsRepository.findOne(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async info(comment_id: string) {
|
async info(comment_id: string) {
|
||||||
const comment = await this.prisma.comments.findUnique({
|
const comment = await this.commentsRepository.findOne(comment_id);
|
||||||
where: { id: comment_id },
|
|
||||||
select: {
|
|
||||||
...selectCommentsWithReplies,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (comment === null) {
|
if (comment === undefined) {
|
||||||
throw new NotFoundException("Comment not found");
|
throw new NotFoundException("Comment not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
return comment;
|
const likes = await this.commentsRepository.countLikes(comment.id);
|
||||||
|
const comments = await this.commentsRepository.countComments(comment.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...comment,
|
||||||
|
count: {
|
||||||
|
likes,
|
||||||
|
comments,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(comment_id: string, user_id: string, content: string) {
|
async update(comment_id: string, user_id: string, content: string) {
|
||||||
let new_content = content;
|
const comment = await this.commentsRepository.findOne(comment_id);
|
||||||
|
|
||||||
const comment = await this.prisma.comments.findFirst({
|
if (comment === undefined) {
|
||||||
where: { id: comment_id },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (comment === null) {
|
|
||||||
throw new NotFoundException("Comment not found");
|
throw new NotFoundException("Comment not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,29 +93,16 @@ export class CommentsService {
|
||||||
throw new UnauthorizedException("Forbidden");
|
throw new UnauthorizedException("Forbidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (comment.content === content.trim()) {
|
const new_content =
|
||||||
new_content = comment.content;
|
comment.content === content.trim() ? comment.content : content;
|
||||||
}
|
|
||||||
|
|
||||||
return await this.prisma.comments.update({
|
return await this.commentsRepository.update(comment_id, new_content);
|
||||||
where: {
|
|
||||||
id: comment_id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
content: new_content,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
...selectCommentsWithReplies,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(comment_id: string, user_id: string) {
|
async delete(comment_id: string, user_id: string) {
|
||||||
const comment = await this.prisma.comments.findFirst({
|
const comment = await this.commentsRepository.findOne(comment_id);
|
||||||
where: { id: comment_id },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (comment === null) {
|
if (comment === undefined) {
|
||||||
throw new NotFoundException("Comment not found");
|
throw new NotFoundException("Comment not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,49 +112,28 @@ export class CommentsService {
|
||||||
|
|
||||||
await this.s3.deleteFiles(comment.attachments);
|
await this.s3.deleteFiles(comment.attachments);
|
||||||
|
|
||||||
await this.prisma.comments.delete({
|
await this.commentsRepository.delete(comment.id);
|
||||||
where: {
|
|
||||||
id: comment_id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
async like(comment_id: string, user_id: string) {
|
async like(comment_id: string, user_id: string) {
|
||||||
const comment = await this.prisma.comments.findFirst({
|
const comment = await this.commentsRepository.findOne(comment_id);
|
||||||
where: {
|
|
||||||
id: comment_id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (comment === null) {
|
if (comment === undefined) {
|
||||||
throw new NotFoundException("Comment not found");
|
throw new NotFoundException("Comment not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const is_comment_already_liked = await this.prisma.commentLike.findFirst({
|
const is_comment_already_liked =
|
||||||
where: {
|
await this.commentsRepository.isAlreadyLiked(comment.id, user_id);
|
||||||
commentId: comment.id,
|
|
||||||
userId: user_id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (is_comment_already_liked !== null) {
|
console.log(is_comment_already_liked);
|
||||||
await this.prisma.commentLike.deleteMany({
|
|
||||||
where: {
|
|
||||||
commentId: comment.id,
|
|
||||||
userId: user_id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
if (is_comment_already_liked) {
|
||||||
|
await this.commentsRepository.dislike(comment.id, user_id);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.prisma.commentLike.create({
|
return await this.commentsRepository.like(user_id, comment.id);
|
||||||
data: {
|
|
||||||
commentId: comment.id,
|
|
||||||
userId: user_id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,24 @@
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { PrismaModule } from "src/services/prisma/prisma.module";
|
import { PrismaService } from "src/services/prisma/prisma.service";
|
||||||
import { S3Service } from "src/services/s3/s3.service";
|
import { S3Service } from "src/services/s3/s3.service";
|
||||||
|
import { UsersRepository } from "src/users/repository/users.repository";
|
||||||
import { CommentsController } from "./comments.controller";
|
import { CommentsController } from "./comments.controller";
|
||||||
import { CommentsService } from "./comments.service";
|
import { CommentsService } from "./comments.service";
|
||||||
import { KweeksController } from "./kweeks.controller";
|
import { KweeksController } from "./kweeks.controller";
|
||||||
import { KweeksService } from "./kweeks.service";
|
import { KweeksService } from "./kweeks.service";
|
||||||
|
import { CommentsRepository } from "./repository/comments.repository";
|
||||||
|
import { KweeksRepository } from "./repository/kweeks.repository";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
|
||||||
controllers: [KweeksController, CommentsController],
|
controllers: [KweeksController, CommentsController],
|
||||||
providers: [KweeksService, S3Service, CommentsService],
|
providers: [
|
||||||
|
PrismaService,
|
||||||
|
KweeksService,
|
||||||
|
S3Service,
|
||||||
|
CommentsService,
|
||||||
|
UsersRepository,
|
||||||
|
CommentsRepository,
|
||||||
|
KweeksRepository,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class KweeksModule {}
|
export class KweeksModule {}
|
||||||
|
|
|
@ -5,14 +5,13 @@ import {
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { PrismaService } from "src/services/prisma/prisma.service";
|
|
||||||
import { S3Service } from "src/services/s3/s3.service";
|
import { S3Service } from "src/services/s3/s3.service";
|
||||||
import { selectComments, selectUser } from "./schemas/prisma_queries.schema";
|
import { KweeksRepository } from "./repository/kweeks.repository";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class KweeksService {
|
export class KweeksService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly kweekRepository: KweeksRepository,
|
||||||
private readonly s3: S3Service,
|
private readonly s3: S3Service,
|
||||||
) {}
|
) {}
|
||||||
async create(content: string, authorId: string, files: Array<File>) {
|
async create(content: string, authorId: string, files: Array<File>) {
|
||||||
|
@ -22,15 +21,7 @@ export class KweeksService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id } = await this.prisma.kweek.create({
|
const { id } = await this.kweekRepository.create(content, authorId);
|
||||||
data: {
|
|
||||||
content,
|
|
||||||
authorId,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
let attachments = [];
|
let attachments = [];
|
||||||
|
|
||||||
|
@ -38,46 +29,34 @@ export class KweeksService {
|
||||||
attachments = await this.s3.multiImageUpload(id, files);
|
attachments = await this.s3.multiImageUpload(id, files);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.prisma.kweek.update({
|
await this.kweekRepository.addAttachments(id, attachments);
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
attachments,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return await this.prisma.kweek.findFirst({ where: { id } });
|
return await this.kweekRepository.findOne(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(id: string) {
|
async findOne(id: string) {
|
||||||
const post = await this.prisma.kweek.findFirst({
|
const post = await this.kweekRepository.findOne(id);
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
author: selectUser,
|
|
||||||
_count: {
|
|
||||||
select: { comments: true, likes: true },
|
|
||||||
},
|
|
||||||
likes: true,
|
|
||||||
comments: selectComments,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (post === null) {
|
if (post === undefined) {
|
||||||
throw new NotFoundException("Post not found");
|
throw new NotFoundException("Post not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
return post;
|
const likes = await this.kweekRepository.countLikes(post.id);
|
||||||
|
const comments = await this.kweekRepository.countComments(post.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...post,
|
||||||
|
count: {
|
||||||
|
likes,
|
||||||
|
comments,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(user_id: string, post_id: string, content: string) {
|
async update(user_id: string, post_id: string, content: string) {
|
||||||
let new_content = content;
|
const post = await this.kweekRepository.findOne(post_id);
|
||||||
|
|
||||||
const post = await this.prisma.kweek.findFirst({ where: { id: post_id } });
|
if (post === undefined) {
|
||||||
|
|
||||||
if (post === null) {
|
|
||||||
throw new NotFoundException("Post not found");
|
throw new NotFoundException("Post not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,37 +64,16 @@ export class KweeksService {
|
||||||
throw new UnauthorizedException("Forbidden");
|
throw new UnauthorizedException("Forbidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (post.content === content.trim()) {
|
const new_content =
|
||||||
new_content = post.content;
|
post.content === content.trim() ? post.content : content;
|
||||||
}
|
|
||||||
|
|
||||||
return await this.prisma.kweek.update({
|
return await this.kweekRepository.update(post_id, new_content);
|
||||||
where: {
|
|
||||||
id: post_id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
content: new_content,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
content: true,
|
|
||||||
attachments: true,
|
|
||||||
createdAt: true,
|
|
||||||
updatedAt: true,
|
|
||||||
author: {
|
|
||||||
select: {
|
|
||||||
displayName: true,
|
|
||||||
username: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(user_id: string, id: string) {
|
async remove(user_id: string, id: string) {
|
||||||
const post = await this.prisma.kweek.findFirst({ where: { id } });
|
const post = await this.kweekRepository.findOne(id);
|
||||||
|
|
||||||
if (post === null) {
|
if (post === undefined) {
|
||||||
throw new NotFoundException("Post not found");
|
throw new NotFoundException("Post not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,49 +83,29 @@ export class KweeksService {
|
||||||
|
|
||||||
await this.s3.deleteFiles(post.attachments);
|
await this.s3.deleteFiles(post.attachments);
|
||||||
|
|
||||||
await this.prisma.kweek.delete({
|
await this.kweekRepository.delete(id);
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
async like(user_id: string, kweek_id: string) {
|
async like(user_id: string, kweek_id: string) {
|
||||||
const kweek = await this.prisma.kweek.findFirst({
|
const kweek = await this.kweekRepository.findOne(kweek_id);
|
||||||
where: {
|
|
||||||
id: kweek_id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (kweek === null) {
|
if (kweek === undefined) {
|
||||||
throw new NotFoundException("Post not found");
|
throw new NotFoundException("Post not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const is_kweek_already_liked = await this.prisma.kweekLike.findFirst({
|
const is_kweek_already_liked = await this.kweekRepository.isAlreadyLiked(
|
||||||
where: {
|
kweek.id,
|
||||||
kweekId: kweek.id,
|
user_id,
|
||||||
userId: user_id,
|
);
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (is_kweek_already_liked !== null) {
|
if (is_kweek_already_liked) {
|
||||||
await this.prisma.kweekLike.deleteMany({
|
await this.kweekRepository.dislike(kweek.id, user_id);
|
||||||
where: {
|
|
||||||
kweekId: kweek.id,
|
|
||||||
userId: user_id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.prisma.kweekLike.create({
|
return await this.kweekRepository.like(user_id, kweek.id);
|
||||||
data: {
|
|
||||||
kweekId: kweek.id,
|
|
||||||
userId: user_id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
117
src/kweeks/repository/comments.repository.ts
Normal file
117
src/kweeks/repository/comments.repository.ts
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { Database } from "src/services/kysely/kysely.service";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CommentsRepository {
|
||||||
|
constructor(private readonly database: Database) {}
|
||||||
|
|
||||||
|
async create(
|
||||||
|
content: string,
|
||||||
|
userId: string,
|
||||||
|
kweekId: string | null,
|
||||||
|
parentId: string | null,
|
||||||
|
): Promise<{ id: string }> {
|
||||||
|
const [comment] = await this.database
|
||||||
|
.insertInto("Comments")
|
||||||
|
.values({
|
||||||
|
id: uuid(),
|
||||||
|
content,
|
||||||
|
userId,
|
||||||
|
kweekId,
|
||||||
|
parentId,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.returning(["id"])
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
return comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, content: string) {
|
||||||
|
return await this.database
|
||||||
|
.updateTable("Comments")
|
||||||
|
.set({ content, updatedAt: new Date() })
|
||||||
|
.where("id", "=", id)
|
||||||
|
.returningAll()
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string) {
|
||||||
|
return await this.database
|
||||||
|
.selectFrom("Comments")
|
||||||
|
.select([
|
||||||
|
"id",
|
||||||
|
"content",
|
||||||
|
"attachments",
|
||||||
|
"createdAt",
|
||||||
|
"userId",
|
||||||
|
"updatedAt",
|
||||||
|
"kweekId",
|
||||||
|
"parentId",
|
||||||
|
])
|
||||||
|
.where("id", "=", id)
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string) {
|
||||||
|
return await this.database
|
||||||
|
.deleteFrom("Comments")
|
||||||
|
.where("id", "=", id)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
async like(userId: string, commentId: string) {
|
||||||
|
return await this.database
|
||||||
|
.insertInto("CommentLike")
|
||||||
|
.values({ userId, commentId })
|
||||||
|
.returningAll()
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
async dislike(commentId: string, userId: string) {
|
||||||
|
await this.database
|
||||||
|
.deleteFrom("CommentLike")
|
||||||
|
.where("commentId", "=", commentId)
|
||||||
|
.where("userId", "=", userId)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
async isAlreadyLiked(commentId: string, userId: string) {
|
||||||
|
return await this.database
|
||||||
|
.selectFrom("CommentLike")
|
||||||
|
.where("commentId", "=", commentId)
|
||||||
|
.where("userId", "=", userId)
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
async addAttachments(commentId: string, attachments: string[]) {
|
||||||
|
return await this.database
|
||||||
|
.updateTable("Comments")
|
||||||
|
.where("id", "=", commentId)
|
||||||
|
.set({ attachments })
|
||||||
|
.returningAll()
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
async countLikes(id: string) {
|
||||||
|
const count = await this.database
|
||||||
|
.selectFrom("CommentLike")
|
||||||
|
.where("commentId", "=", id)
|
||||||
|
.select(this.database.fn.countAll<number>().as("count"))
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
|
return count.count ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async countComments(id: string) {
|
||||||
|
const count = await this.database
|
||||||
|
.selectFrom("Comments")
|
||||||
|
.where((qb) => qb("kweekId", "=", id).or("parentId", "=", id))
|
||||||
|
.select(this.database.fn.countAll<number>().as("count"))
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
|
return count.count ?? 0;
|
||||||
|
}
|
||||||
|
}
|
99
src/kweeks/repository/kweeks.repository.ts
Normal file
99
src/kweeks/repository/kweeks.repository.ts
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { Database } from "src/services/kysely/kysely.service";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class KweeksRepository {
|
||||||
|
constructor(private readonly database: Database) {}
|
||||||
|
|
||||||
|
async create(content: string, authorId: string): Promise<{ id: string }> {
|
||||||
|
const [kweek] = await this.database
|
||||||
|
.insertInto("Kweek")
|
||||||
|
.values({
|
||||||
|
id: uuid(),
|
||||||
|
content,
|
||||||
|
authorId,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.returning(["id"])
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
return kweek;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, content: string) {
|
||||||
|
return await this.database
|
||||||
|
.updateTable("Kweek")
|
||||||
|
.set({ content, updatedAt: new Date() })
|
||||||
|
.where("id", "=", id)
|
||||||
|
.returningAll()
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string) {
|
||||||
|
return await this.database
|
||||||
|
.selectFrom("Kweek")
|
||||||
|
.selectAll()
|
||||||
|
.where("id", "=", id)
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string) {
|
||||||
|
return await this.database
|
||||||
|
.deleteFrom("Kweek")
|
||||||
|
.where("id", "=", id)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
async like(userId: string, kweekId: string) {
|
||||||
|
return await this.database
|
||||||
|
.insertInto("KweekLike")
|
||||||
|
.values({ userId, kweekId })
|
||||||
|
.returningAll()
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
async dislike(kweekId: string, userId: string) {
|
||||||
|
await this.database
|
||||||
|
.deleteFrom("KweekLike")
|
||||||
|
.where("kweekId", "=", kweekId)
|
||||||
|
.where("userId", "=", userId)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
async isAlreadyLiked(kweekId: string, userId: string) {
|
||||||
|
return await this.database
|
||||||
|
.selectFrom("KweekLike")
|
||||||
|
.where("kweekId", "=", kweekId)
|
||||||
|
.where("userId", "=", userId)
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
async addAttachments(kweekId: string, attachments: string[]) {
|
||||||
|
return await this.database
|
||||||
|
.updateTable("Kweek")
|
||||||
|
.where("id", "=", kweekId)
|
||||||
|
.set({ attachments })
|
||||||
|
.returningAll()
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
async countLikes(id: string) {
|
||||||
|
const count = await this.database
|
||||||
|
.selectFrom("KweekLike")
|
||||||
|
.where("kweekId", "=", id)
|
||||||
|
.select(this.database.fn.countAll<number>().as("count"))
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
return count.count ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async countComments(id: string) {
|
||||||
|
const count = await this.database
|
||||||
|
.selectFrom("Comments")
|
||||||
|
.where("kweekId", "=", id)
|
||||||
|
.select(this.database.fn.countAll<number>().as("count"))
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
return count.count ?? 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,6 +20,8 @@ import { Configuration } from "./configuration";
|
||||||
TODO: Create the chat system.
|
TODO: Create the chat system.
|
||||||
-> Initialize the websocket system first.
|
-> Initialize the websocket system first.
|
||||||
TODO: Create a TOS.
|
TODO: Create a TOS.
|
||||||
|
TODO: Improve Kysely Queries.
|
||||||
|
TODO: Fix Docker Image.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
|
|
16
src/services/kysely/kysely.definition.ts
Normal file
16
src/services/kysely/kysely.definition.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { ConfigurableModuleBuilder } from "@nestjs/common";
|
||||||
|
|
||||||
|
export interface DatabaseOptions {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
user: string;
|
||||||
|
password: string;
|
||||||
|
database: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const {
|
||||||
|
ConfigurableModuleClass: ConfigurableDatabaseModule,
|
||||||
|
MODULE_OPTIONS_TOKEN: DATABASE_OPTIONS,
|
||||||
|
} = new ConfigurableModuleBuilder<DatabaseOptions>()
|
||||||
|
.setClassMethodName("forRoot")
|
||||||
|
.build();
|
40
src/services/kysely/kysely.module.ts
Normal file
40
src/services/kysely/kysely.module.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import { Global, Module } from "@nestjs/common";
|
||||||
|
import { PostgresDialect } from "kysely";
|
||||||
|
import { Pool } from "pg";
|
||||||
|
import {
|
||||||
|
ConfigurableDatabaseModule,
|
||||||
|
DATABASE_OPTIONS,
|
||||||
|
DatabaseOptions,
|
||||||
|
} from "./kysely.definition";
|
||||||
|
import { Database } from "./kysely.service";
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
exports: [Database],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: Database,
|
||||||
|
inject: [DATABASE_OPTIONS],
|
||||||
|
useFactory: ({
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
user,
|
||||||
|
password,
|
||||||
|
database,
|
||||||
|
}: DatabaseOptions) => {
|
||||||
|
const dialect = new PostgresDialect({
|
||||||
|
pool: new Pool({
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
user,
|
||||||
|
password,
|
||||||
|
database,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Database({ dialect });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class KyselyModule extends ConfigurableDatabaseModule {}
|
4
src/services/kysely/kysely.service.ts
Normal file
4
src/services/kysely/kysely.service.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { Kysely } from "kysely";
|
||||||
|
import { DB } from "src/db/types";
|
||||||
|
|
||||||
|
export class Database extends Kysely<DB> {}
|
171
src/users/repository/users.repository.ts
Normal file
171
src/users/repository/users.repository.ts
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { Database } from "src/services/kysely/kysely.service";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
import { UserModel } from "../models/user.model";
|
||||||
|
import { User } from "../types/user.type";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UsersRepository {
|
||||||
|
constructor(private readonly database: Database) {}
|
||||||
|
|
||||||
|
async authSearch(identifier: string): Promise<UserModel | undefined> {
|
||||||
|
const user = await this.database
|
||||||
|
.selectFrom("User")
|
||||||
|
.selectAll()
|
||||||
|
.where((eb) =>
|
||||||
|
eb.or([eb("username", "=", identifier), eb("id", "=", identifier)]),
|
||||||
|
)
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
return user ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id: string): Promise<UserModel | undefined> {
|
||||||
|
const user = await this.database
|
||||||
|
.selectFrom("User")
|
||||||
|
.select(["id", "displayName", "username", "createdAt"])
|
||||||
|
.where("id", "=", id)
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
return user ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByUsername(username: string): Promise<UserModel | undefined> {
|
||||||
|
const user = await this.database
|
||||||
|
.selectFrom("User")
|
||||||
|
.select(["id", "displayName", "username", "createdAt"])
|
||||||
|
.where("username", "=", username)
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
return user ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByEmail(email: string): Promise<UserModel | undefined> {
|
||||||
|
const user = await this.database
|
||||||
|
.selectFrom("User")
|
||||||
|
.select(["id", "displayName", "username", "createdAt"])
|
||||||
|
.where("email", "=", email)
|
||||||
|
.executeTakeFirst();
|
||||||
|
return user ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(data: {
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}): Promise<Pick<UserModel, "displayName" | "username" | "createdAt">> {
|
||||||
|
const user = this.database
|
||||||
|
.insertInto("User")
|
||||||
|
.values({
|
||||||
|
id: uuid(),
|
||||||
|
username: data.username,
|
||||||
|
email: data.email,
|
||||||
|
password: data.password,
|
||||||
|
createdAt: new Date(),
|
||||||
|
})
|
||||||
|
.returning(["displayName", "username", "createdAt"])
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async countFollowers(id: string): Promise<number> {
|
||||||
|
const count = await this.database
|
||||||
|
.selectFrom("Follows")
|
||||||
|
.where("followerId", "=", id)
|
||||||
|
.select(this.database.fn.countAll<number>().as("count"))
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
|
return count.count ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async countFollowing(id: string): Promise<number> {
|
||||||
|
const count = await this.database
|
||||||
|
.selectFrom("Follows")
|
||||||
|
.where("followingId", "=", id)
|
||||||
|
.select(this.database.fn.countAll<number>().as("count"))
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
|
return count.count ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserKweeks(id: string) {
|
||||||
|
const kweeks = await this.database
|
||||||
|
.selectFrom("Kweek")
|
||||||
|
.where("authorId", "=", id)
|
||||||
|
.select(["id", "content", "attachments", "createdAt", "updatedAt"])
|
||||||
|
.execute();
|
||||||
|
return kweeks;
|
||||||
|
}
|
||||||
|
async updateEmail(id: string, email: string): Promise<void> {
|
||||||
|
await this.database
|
||||||
|
.updateTable("User")
|
||||||
|
.set({ email })
|
||||||
|
.where("id", "=", id)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUsername(
|
||||||
|
id: string,
|
||||||
|
username: string | undefined,
|
||||||
|
displayName: string | undefined,
|
||||||
|
): Promise<Pick<User, "username" | "displayName">> {
|
||||||
|
const user = await this.database
|
||||||
|
.updateTable("User")
|
||||||
|
.set({ username, displayName })
|
||||||
|
.where("id", "=", id)
|
||||||
|
.returning(["username", "displayName"])
|
||||||
|
.executeTakeFirst();
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updatePassword(id: string, password: string): Promise<void> {
|
||||||
|
await this.database
|
||||||
|
.updateTable("User")
|
||||||
|
.set({ password })
|
||||||
|
.where("id", "=", id)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateProfileImage(
|
||||||
|
id: string,
|
||||||
|
url: string,
|
||||||
|
): Promise<{ profileImage: string }> {
|
||||||
|
return await this.database
|
||||||
|
.updateTable("User")
|
||||||
|
.set({ profileImage: url })
|
||||||
|
.where("id", "=", id)
|
||||||
|
.returning(["profileImage"])
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string): Promise<void> {
|
||||||
|
await this.database.deleteFrom("User").where("id", "=", id).execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
async isFollowing(followerId: string, followingId: string): Promise<boolean> {
|
||||||
|
const follows = await this.database
|
||||||
|
.selectFrom("Follows")
|
||||||
|
.where("followerId", "=", followerId)
|
||||||
|
.where("followingId", "=", followingId)
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
return follows !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async follow(followerId: string, followingId: string) {
|
||||||
|
return await this.database
|
||||||
|
.insertInto("Follows")
|
||||||
|
.values({ followerId, followingId })
|
||||||
|
.returning(["followingId", "followerId"])
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
async unfollow(followerId: string, followingId) {
|
||||||
|
return await this.database
|
||||||
|
.deleteFrom("Follows")
|
||||||
|
.where("followerId", "=", followerId)
|
||||||
|
.where("followingId", "=", followingId)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,12 @@
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { PrismaModule } from "src/services/prisma/prisma.module";
|
|
||||||
import { S3Service } from "src/services/s3/s3.service";
|
import { S3Service } from "src/services/s3/s3.service";
|
||||||
|
import { UsersRepository } from "./repository/users.repository";
|
||||||
import { UserController } from "./users.controller";
|
import { UserController } from "./users.controller";
|
||||||
import { UserService } from "./users.service";
|
import { UserService } from "./users.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
|
||||||
controllers: [UserController],
|
controllers: [UserController],
|
||||||
providers: [UserService, S3Service],
|
providers: [UserService, S3Service, UsersRepository],
|
||||||
exports: [UserService],
|
exports: [UserService],
|
||||||
})
|
})
|
||||||
export class UserModule {}
|
export class UserModule {}
|
||||||
|
|
|
@ -5,76 +5,38 @@ import {
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import * as argon2 from "argon2";
|
import * as argon2 from "argon2";
|
||||||
import { PrismaService } from "src/services/prisma/prisma.service";
|
|
||||||
import { S3Service } from "src/services/s3/s3.service";
|
import { S3Service } from "src/services/s3/s3.service";
|
||||||
import { CreateUserDTO } from "./dto/create_user.dto";
|
import { CreateUserDTO } from "./dto/create_user.dto";
|
||||||
import { UserModel } from "./models/user.model";
|
import { UserModel } from "./models/user.model";
|
||||||
|
import { UsersRepository } from "./repository/users.repository";
|
||||||
import { User } from "./types/user.type";
|
import { User } from "./types/user.type";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserService {
|
export class UserService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
|
||||||
private readonly s3: S3Service,
|
private readonly s3: S3Service,
|
||||||
|
private readonly userRepository: UsersRepository,
|
||||||
) {}
|
) {}
|
||||||
async auth_search(username: string): Promise<UserModel> {
|
async auth_search(username: string): Promise<UserModel> {
|
||||||
const user = await this.prisma.user.findFirst({
|
return await this.userRepository.authSearch(username);
|
||||||
where: {
|
|
||||||
username,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
profileImage: true,
|
|
||||||
displayName: true,
|
|
||||||
username: true,
|
|
||||||
password: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (user == null) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return user;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async info(username: string): Promise<UserModel> {
|
async info(username: string): Promise<UserModel> {
|
||||||
const user = await this.prisma.user.findFirst({
|
const user = await this.userRepository.findByUsername(username);
|
||||||
where: { username },
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
profileImage: true,
|
|
||||||
displayName: true,
|
|
||||||
username: true,
|
|
||||||
createdAt: true,
|
|
||||||
followers: true,
|
|
||||||
following: true,
|
|
||||||
kweeks: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
content: true,
|
|
||||||
attachments: true,
|
|
||||||
createdAt: true,
|
|
||||||
updatedAt: true,
|
|
||||||
_count: {
|
|
||||||
select: {
|
|
||||||
comments: true,
|
|
||||||
likes: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (user === null) {
|
if (user === undefined) {
|
||||||
throw new NotFoundException("User not found");
|
throw new NotFoundException("User not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const followers = await this.userRepository.countFollowers(user.id);
|
||||||
|
const following = await this.userRepository.countFollowing(user.id);
|
||||||
|
const kweeks = await this.userRepository.getUserKweeks(user.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...user,
|
...user,
|
||||||
followers: user.followers.length,
|
followers,
|
||||||
following: user.following.length,
|
following,
|
||||||
|
kweeks,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,88 +47,56 @@ export class UserService {
|
||||||
}: CreateUserDTO): Promise<
|
}: CreateUserDTO): Promise<
|
||||||
Pick<UserModel, "displayName" | "username" | "createdAt">
|
Pick<UserModel, "displayName" | "username" | "createdAt">
|
||||||
> {
|
> {
|
||||||
if ((await this.prisma.user.findFirst({ where: { username } })) != null) {
|
if ((await this.userRepository.findByUsername(username)) !== undefined) {
|
||||||
throw new BadRequestException("Username already in use");
|
throw new BadRequestException("Username already in use");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((await this.prisma.user.findFirst({ where: { email } })) != null) {
|
if ((await this.userRepository.findByEmail(email)) !== undefined) {
|
||||||
throw new BadRequestException("Email already in use");
|
throw new BadRequestException("Email already in use");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Password encryption
|
|
||||||
const hash = await argon2.hash(password);
|
const hash = await argon2.hash(password);
|
||||||
|
|
||||||
const user = await this.prisma.user.create({
|
return await this.userRepository.create({
|
||||||
data: {
|
username,
|
||||||
username,
|
email,
|
||||||
email,
|
password: hash,
|
||||||
password: hash,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
displayName: true,
|
|
||||||
username: true,
|
|
||||||
createdAt: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return user;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async follow(authenticated_id: string, username: string) {
|
async follow(authenticated_id: string, username: string) {
|
||||||
const user_to_follow = await this.prisma.user.findFirst({
|
const user_to_follow = await this.userRepository.findByUsername(username);
|
||||||
where: { username },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (user_to_follow === null) {
|
if (user_to_follow === undefined) {
|
||||||
throw new NotFoundException("User to follow not found");
|
throw new NotFoundException("User to follow not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const is_already_following = await this.prisma.follows.findFirst({
|
const is_already_following = await this.userRepository.isFollowing(
|
||||||
where: {
|
user_to_follow.id,
|
||||||
followerId: user_to_follow.id,
|
authenticated_id,
|
||||||
followingId: authenticated_id,
|
);
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (is_already_following !== null) {
|
if (is_already_following) {
|
||||||
await this.prisma.follows.deleteMany({
|
await this.userRepository.unfollow(user_to_follow.id, authenticated_id);
|
||||||
where: {
|
|
||||||
followerId: user_to_follow.id,
|
|
||||||
followingId: authenticated_id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.prisma.follows.create({
|
return await this.userRepository.follow(
|
||||||
data: {
|
user_to_follow.id,
|
||||||
followerId: user_to_follow.id,
|
authenticated_id,
|
||||||
followingId: authenticated_id,
|
);
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateEmail(id: string, email: string): Promise<{ message: string }> {
|
async updateEmail(id: string, email: string): Promise<{ message: string }> {
|
||||||
const user = await this.prisma.user.findFirst({
|
const user = await this.userRepository.findById(id);
|
||||||
where: { id },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (email !== undefined && email.trim() !== user.email) {
|
if (email !== undefined && email.trim() !== user.email) {
|
||||||
const isAlreadyInUse = await this.prisma.user.findFirst({
|
const isAlreadyInUse = await this.userRepository.findByEmail(email);
|
||||||
where: { email },
|
if (isAlreadyInUse !== undefined && isAlreadyInUse.email !== user.email) {
|
||||||
});
|
|
||||||
if (isAlreadyInUse != null && isAlreadyInUse.email !== user.email) {
|
|
||||||
throw new BadRequestException("Email already in use");
|
throw new BadRequestException("Email already in use");
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.prisma.user.update({
|
await this.userRepository.updateEmail(id, email);
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
email: email ?? user.email,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return { message: "Email updated successfully" };
|
return { message: "Email updated successfully" };
|
||||||
}
|
}
|
||||||
|
@ -177,32 +107,19 @@ export class UserService {
|
||||||
username: string | undefined,
|
username: string | undefined,
|
||||||
displayName: string,
|
displayName: string,
|
||||||
): Promise<Pick<User, "username" | "displayName">> {
|
): Promise<Pick<User, "username" | "displayName">> {
|
||||||
const user = await this.prisma.user.findFirst({
|
const user = await this.userRepository.findById(id);
|
||||||
where: { id },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (username !== undefined && username.trim() !== user.username) {
|
if (username !== undefined && username.trim() !== user.username) {
|
||||||
const isAlreadyInUse = await this.prisma.user.findFirst({
|
const isAlreadyInUse = await this.userRepository.findByUsername(username);
|
||||||
where: { username },
|
if (
|
||||||
});
|
isAlreadyInUse !== undefined &&
|
||||||
if (isAlreadyInUse != null && isAlreadyInUse.username !== user.username) {
|
isAlreadyInUse.username !== user.username
|
||||||
|
) {
|
||||||
throw new BadRequestException("Username already in use");
|
throw new BadRequestException("Username already in use");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.prisma.user.update({
|
return await this.userRepository.updateUsername(id, username, displayName);
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
displayName,
|
|
||||||
username: username ?? user.username,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
displayName: true,
|
|
||||||
username: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updatePassword(
|
async updatePassword(
|
||||||
|
@ -210,9 +127,7 @@ export class UserService {
|
||||||
old_password: string,
|
old_password: string,
|
||||||
new_password: string,
|
new_password: string,
|
||||||
): Promise<{ message: string }> {
|
): Promise<{ message: string }> {
|
||||||
const user = await this.prisma.user.findFirst({
|
const user = await this.userRepository.authSearch(id);
|
||||||
where: { id },
|
|
||||||
});
|
|
||||||
|
|
||||||
const validatePassword = await argon2.verify(user.password, old_password);
|
const validatePassword = await argon2.verify(user.password, old_password);
|
||||||
|
|
||||||
|
@ -222,14 +137,7 @@ export class UserService {
|
||||||
|
|
||||||
const hash = await argon2.hash(new_password);
|
const hash = await argon2.hash(new_password);
|
||||||
|
|
||||||
await this.prisma.user.update({
|
await this.userRepository.updatePassword(id, hash);
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
password: hash,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return { message: "Password updated successfully" };
|
return { message: "Password updated successfully" };
|
||||||
}
|
}
|
||||||
|
@ -237,27 +145,11 @@ export class UserService {
|
||||||
async uploadImage(id: string, image: File) {
|
async uploadImage(id: string, image: File) {
|
||||||
const url = await this.s3.uploadImage(id, image.buffer);
|
const url = await this.s3.uploadImage(id, image.buffer);
|
||||||
|
|
||||||
return await this.prisma.user.update({
|
return await this.userRepository.updateProfileImage(id, url);
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
profileImage: url,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
profileImage: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: string) {
|
async delete(id: string) {
|
||||||
// TODO: Add validation for safety (like e-mail confirmation or password)
|
await this.userRepository.delete(id);
|
||||||
// TODO: Delete the user's attachments when deleting, like Kweeks attachments and profile pictures.
|
return { message: "User deleted" };
|
||||||
try {
|
|
||||||
await this.prisma.user.deleteMany({ where: { id } });
|
|
||||||
return { message: "User deleted" };
|
|
||||||
} catch (e) {
|
|
||||||
throw new BadRequestException("Error while trying to delete user");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue