feat(folders): 添加公开文件夹和收藏功能
- 新增文件夹可见性控制(公开/私有) - 添加公开文件夹浏览和搜索 - 实现文件夹收藏功能 - 新增 FolderFavorite 数据模型 - 更新 Prisma 至 7.4.2 - 添加相关 i18n 翻译
This commit is contained in:
@@ -3,15 +3,13 @@
|
||||
import { auth } from "@/auth";
|
||||
import { headers } from "next/headers";
|
||||
import { ValidateError } from "@/lib/errors";
|
||||
import { ActionInputCreatePair, ActionInputUpdatePairById, ActionOutputGetFoldersWithTotalPairsByUserId, validateActionInputCreatePair, validateActionInputUpdatePairById } from "./folder-action-dto";
|
||||
import { repoCreateFolder, repoCreatePair, repoDeleteFolderById, repoDeletePairById, repoGetFolderIdByPairId, repoGetFoldersByUserId, repoGetFoldersWithTotalPairsByUserId, repoGetPairsByFolderId, repoGetUserIdByFolderId, repoRenameFolderById, repoUpdatePairById } from "./folder-repository";
|
||||
import { ActionInputCreatePair, ActionInputUpdatePairById, ActionOutputGetFoldersWithTotalPairsByUserId, ActionOutputGetPublicFolders, ActionOutputSetFolderVisibility, ActionOutputToggleFavorite, ActionOutputCheckFavorite, validateActionInputCreatePair, validateActionInputUpdatePairById } from "./folder-action-dto";
|
||||
import { repoCreateFolder, repoCreatePair, repoDeleteFolderById, repoDeletePairById, repoGetFolderIdByPairId, repoGetFolderVisibility, repoGetFoldersByUserId, repoGetFoldersWithTotalPairsByUserId, repoGetPairsByFolderId, repoGetPublicFolders, repoGetUserIdByFolderId, repoRenameFolderById, repoSearchPublicFolders, repoUpdateFolderVisibility, repoUpdatePairById, repoToggleFavorite, repoCheckFavorite } from "./folder-repository";
|
||||
import { validate } from "@/utils/validate";
|
||||
import z from "zod";
|
||||
import { LENGTH_MAX_FOLDER_NAME, LENGTH_MIN_FOLDER_NAME } from "@/shared/constant";
|
||||
import { Visibility } from "../../../generated/prisma/enums";
|
||||
|
||||
/**
|
||||
* Helper function to check if the current user is the owner of a folder
|
||||
*/
|
||||
async function checkFolderOwnership(folderId: number): Promise<boolean> {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
if (!session?.user?.id) return false;
|
||||
@@ -20,9 +18,6 @@ async function checkFolderOwnership(folderId: number): Promise<boolean> {
|
||||
return folderOwnerId === session.user.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to check if the current user is the owner of a pair's folder
|
||||
*/
|
||||
async function checkPairOwnership(pairId: number): Promise<boolean> {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
if (!session?.user?.id) return false;
|
||||
@@ -52,7 +47,6 @@ export async function actionGetPairsByFolderId(folderId: number) {
|
||||
|
||||
export async function actionUpdatePairById(id: number, dto: ActionInputUpdatePairById) {
|
||||
try {
|
||||
// Check ownership
|
||||
const isOwner = await checkPairOwnership(id);
|
||||
if (!isOwner) {
|
||||
return {
|
||||
@@ -92,9 +86,24 @@ export async function actionGetUserIdByFolderId(folderId: number) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionGetFolderVisibility(folderId: number) {
|
||||
try {
|
||||
return {
|
||||
success: true,
|
||||
message: 'success',
|
||||
data: await repoGetFolderVisibility(folderId)
|
||||
};
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Unknown error occured.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionDeleteFolderById(folderId: number) {
|
||||
try {
|
||||
// Check ownership
|
||||
const isOwner = await checkFolderOwnership(folderId);
|
||||
if (!isOwner) {
|
||||
return {
|
||||
@@ -119,7 +128,6 @@ export async function actionDeleteFolderById(folderId: number) {
|
||||
|
||||
export async function actionDeletePairById(id: number) {
|
||||
try {
|
||||
// Check ownership
|
||||
const isOwner = await checkPairOwnership(id);
|
||||
if (!isOwner) {
|
||||
return {
|
||||
@@ -176,7 +184,6 @@ export async function actionGetFoldersByUserId(userId: string) {
|
||||
|
||||
export async function actionCreatePair(dto: ActionInputCreatePair) {
|
||||
try {
|
||||
// Check ownership
|
||||
const isOwner = await checkFolderOwnership(dto.folderId);
|
||||
if (!isOwner) {
|
||||
return {
|
||||
@@ -238,7 +245,6 @@ export async function actionCreateFolder(userId: string, folderName: string) {
|
||||
|
||||
export async function actionRenameFolderById(id: number, newName: string) {
|
||||
try {
|
||||
// Check ownership
|
||||
const isOwner = await checkFolderOwnership(id);
|
||||
if (!isOwner) {
|
||||
return {
|
||||
@@ -272,3 +278,150 @@ export async function actionRenameFolderById(id: number, newName: string) {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionSetFolderVisibility(
|
||||
folderId: number,
|
||||
visibility: "PRIVATE" | "PUBLIC",
|
||||
): Promise<ActionOutputSetFolderVisibility> {
|
||||
try {
|
||||
const isOwner = await checkFolderOwnership(folderId);
|
||||
if (!isOwner) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'You do not have permission to change this folder visibility.',
|
||||
};
|
||||
}
|
||||
|
||||
await repoUpdateFolderVisibility({
|
||||
folderId,
|
||||
visibility: visibility as Visibility,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: 'success',
|
||||
};
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Unknown error occured.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionGetPublicFolders(): Promise<ActionOutputGetPublicFolders> {
|
||||
try {
|
||||
const data = await repoGetPublicFolders({});
|
||||
return {
|
||||
success: true,
|
||||
message: 'success',
|
||||
data: data.map((folder) => ({
|
||||
...folder,
|
||||
visibility: folder.visibility as "PRIVATE" | "PUBLIC",
|
||||
})),
|
||||
};
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Unknown error occured.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionSearchPublicFolders(query: string): Promise<ActionOutputGetPublicFolders> {
|
||||
try {
|
||||
const data = await repoSearchPublicFolders({ query, limit: 50 });
|
||||
return {
|
||||
success: true,
|
||||
message: 'success',
|
||||
data: data.map((folder) => ({
|
||||
...folder,
|
||||
visibility: folder.visibility as "PRIVATE" | "PUBLIC",
|
||||
})),
|
||||
};
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Unknown error occured.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionToggleFavorite(
|
||||
folderId: number,
|
||||
): Promise<ActionOutputToggleFavorite> {
|
||||
try {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
if (!session?.user?.id) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Unauthorized',
|
||||
};
|
||||
}
|
||||
|
||||
const isFavorited = await repoToggleFavorite({
|
||||
folderId,
|
||||
userId: session.user.id,
|
||||
});
|
||||
|
||||
const { favoriteCount } = await repoCheckFavorite({
|
||||
folderId,
|
||||
userId: session.user.id,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'success',
|
||||
data: {
|
||||
isFavorited,
|
||||
favoriteCount,
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Unknown error occured.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionCheckFavorite(
|
||||
folderId: number,
|
||||
): Promise<ActionOutputCheckFavorite> {
|
||||
try {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
if (!session?.user?.id) {
|
||||
return {
|
||||
success: true,
|
||||
message: 'success',
|
||||
data: {
|
||||
isFavorited: false,
|
||||
favoriteCount: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const { isFavorited, favoriteCount } = await repoCheckFavorite({
|
||||
folderId,
|
||||
userId: session.user.id,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'success',
|
||||
data: {
|
||||
isFavorited,
|
||||
favoriteCount,
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Unknown error occured.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,3 +32,55 @@ export type ActionOutputGetFoldersWithTotalPairsByUserId = {
|
||||
success: boolean,
|
||||
data?: TSharedFolderWithTotalPairs[];
|
||||
};
|
||||
|
||||
export const schemaActionInputSetFolderVisibility = z.object({
|
||||
folderId: z.number().int().positive(),
|
||||
visibility: z.enum(["PRIVATE", "PUBLIC"]),
|
||||
});
|
||||
export type ActionInputSetFolderVisibility = z.infer<typeof schemaActionInputSetFolderVisibility>;
|
||||
|
||||
export const schemaActionInputSearchPublicFolders = z.object({
|
||||
query: z.string().min(1).max(100),
|
||||
});
|
||||
export type ActionInputSearchPublicFolders = z.infer<typeof schemaActionInputSearchPublicFolders>;
|
||||
|
||||
export type ActionOutputPublicFolder = {
|
||||
id: number;
|
||||
name: string;
|
||||
visibility: "PRIVATE" | "PUBLIC";
|
||||
createdAt: Date;
|
||||
userId: string;
|
||||
userName: string | null;
|
||||
userUsername: string | null;
|
||||
totalPairs: number;
|
||||
favoriteCount: number;
|
||||
};
|
||||
|
||||
export type ActionOutputGetPublicFolders = {
|
||||
message: string;
|
||||
success: boolean;
|
||||
data?: ActionOutputPublicFolder[];
|
||||
};
|
||||
|
||||
export type ActionOutputSetFolderVisibility = {
|
||||
message: string;
|
||||
success: boolean;
|
||||
};
|
||||
|
||||
export type ActionOutputToggleFavorite = {
|
||||
message: string;
|
||||
success: boolean;
|
||||
data?: {
|
||||
isFavorited: boolean;
|
||||
favoriteCount: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type ActionOutputCheckFavorite = {
|
||||
message: string;
|
||||
success: boolean;
|
||||
data?: {
|
||||
isFavorited: boolean;
|
||||
favoriteCount: number;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Visibility } from "../../../generated/prisma/enums";
|
||||
|
||||
export interface RepoInputCreateFolder {
|
||||
name: string;
|
||||
userId: string;
|
||||
@@ -21,3 +23,51 @@ export interface RepoInputUpdatePair {
|
||||
ipa1?: string;
|
||||
ipa2?: string;
|
||||
}
|
||||
|
||||
export interface RepoInputUpdateFolderVisibility {
|
||||
folderId: number;
|
||||
visibility: Visibility;
|
||||
}
|
||||
|
||||
export interface RepoInputSearchPublicFolders {
|
||||
query: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface RepoInputGetPublicFolders {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
orderBy?: "createdAt" | "name";
|
||||
}
|
||||
|
||||
export type RepoOutputPublicFolder = {
|
||||
id: number;
|
||||
name: string;
|
||||
visibility: Visibility;
|
||||
createdAt: Date;
|
||||
userId: string;
|
||||
userName: string | null;
|
||||
userUsername: string | null;
|
||||
totalPairs: number;
|
||||
favoriteCount: number;
|
||||
};
|
||||
|
||||
export type RepoOutputFolderVisibility = {
|
||||
visibility: Visibility;
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export interface RepoInputToggleFavorite {
|
||||
folderId: number;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface RepoInputCheckFavorite {
|
||||
folderId: number;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export type RepoOutputFavoriteStatus = {
|
||||
isFavorited: boolean;
|
||||
favoriteCount: number;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { RepoInputCreateFolder, RepoInputCreatePair, RepoInputUpdatePair } from "./folder-repository-dto";
|
||||
import {
|
||||
RepoInputCreateFolder,
|
||||
RepoInputCreatePair,
|
||||
RepoInputUpdatePair,
|
||||
RepoInputUpdateFolderVisibility,
|
||||
RepoInputSearchPublicFolders,
|
||||
RepoInputGetPublicFolders,
|
||||
RepoOutputPublicFolder,
|
||||
RepoOutputFolderVisibility,
|
||||
RepoInputToggleFavorite,
|
||||
RepoInputCheckFavorite,
|
||||
RepoOutputFavoriteStatus,
|
||||
} from "./folder-repository-dto";
|
||||
import { Visibility } from "../../../generated/prisma/enums";
|
||||
|
||||
export async function repoCreatePair(data: RepoInputCreatePair) {
|
||||
return (await prisma.pair.create({
|
||||
@@ -63,7 +76,8 @@ export async function repoGetFoldersByUserId(userId: string) {
|
||||
return {
|
||||
id: v.id,
|
||||
name: v.name,
|
||||
userId: v.userId
|
||||
userId: v.userId,
|
||||
visibility: v.visibility,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -95,6 +109,7 @@ export async function repoGetFoldersWithTotalPairsByUserId(userId: string) {
|
||||
id: folder.id,
|
||||
name: folder.name,
|
||||
userId: folder.userId,
|
||||
visibility: folder.visibility,
|
||||
total: folder._count?.pairs ?? 0,
|
||||
createdAt: folder.createdAt,
|
||||
}));
|
||||
@@ -134,3 +149,126 @@ export async function repoGetFolderIdByPairId(pairId: number) {
|
||||
});
|
||||
return pair?.folderId;
|
||||
}
|
||||
|
||||
export async function repoUpdateFolderVisibility(
|
||||
input: RepoInputUpdateFolderVisibility,
|
||||
): Promise<void> {
|
||||
await prisma.folder.update({
|
||||
where: { id: input.folderId },
|
||||
data: { visibility: input.visibility },
|
||||
});
|
||||
}
|
||||
|
||||
export async function repoGetFolderVisibility(
|
||||
folderId: number,
|
||||
): Promise<RepoOutputFolderVisibility | null> {
|
||||
const folder = await prisma.folder.findUnique({
|
||||
where: { id: folderId },
|
||||
select: { visibility: true, userId: true },
|
||||
});
|
||||
return folder;
|
||||
}
|
||||
|
||||
export async function repoGetPublicFolders(
|
||||
input: RepoInputGetPublicFolders = {},
|
||||
): Promise<RepoOutputPublicFolder[]> {
|
||||
const { limit = 50, offset = 0, orderBy = "createdAt" } = input;
|
||||
|
||||
const folders = await prisma.folder.findMany({
|
||||
where: { visibility: Visibility.PUBLIC },
|
||||
include: {
|
||||
_count: { select: { pairs: true, favorites: true } },
|
||||
user: { select: { name: true, username: true } },
|
||||
},
|
||||
orderBy: { [orderBy]: "desc" },
|
||||
take: limit,
|
||||
skip: offset,
|
||||
});
|
||||
return folders.map((folder) => ({
|
||||
id: folder.id,
|
||||
name: folder.name,
|
||||
visibility: folder.visibility,
|
||||
createdAt: folder.createdAt,
|
||||
userId: folder.userId,
|
||||
userName: folder.user.name,
|
||||
userUsername: folder.user.username,
|
||||
totalPairs: folder._count.pairs,
|
||||
favoriteCount: folder._count.favorites,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function repoSearchPublicFolders(
|
||||
input: RepoInputSearchPublicFolders,
|
||||
): Promise<RepoOutputPublicFolder[]> {
|
||||
const { query, limit = 50 } = input;
|
||||
const folders = await prisma.folder.findMany({
|
||||
where: {
|
||||
visibility: Visibility.PUBLIC,
|
||||
name: { contains: query, mode: "insensitive" },
|
||||
},
|
||||
include: {
|
||||
_count: { select: { pairs: true, favorites: true } },
|
||||
user: { select: { name: true, username: true } },
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: limit,
|
||||
});
|
||||
return folders.map((folder) => ({
|
||||
id: folder.id,
|
||||
name: folder.name,
|
||||
visibility: folder.visibility,
|
||||
createdAt: folder.createdAt,
|
||||
userId: folder.userId,
|
||||
userName: folder.user.name,
|
||||
userUsername: folder.user.username,
|
||||
totalPairs: folder._count.pairs,
|
||||
favoriteCount: folder._count.favorites,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function repoToggleFavorite(
|
||||
input: RepoInputToggleFavorite,
|
||||
): Promise<boolean> {
|
||||
const existing = await prisma.folderFavorite.findUnique({
|
||||
where: {
|
||||
userId_folderId: {
|
||||
userId: input.userId,
|
||||
folderId: input.folderId,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (existing) {
|
||||
await prisma.folderFavorite.delete({
|
||||
where: { id: existing.id },
|
||||
});
|
||||
return false;
|
||||
} else {
|
||||
await prisma.folderFavorite.create({
|
||||
data: {
|
||||
userId: input.userId,
|
||||
folderId: input.folderId,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export async function repoCheckFavorite(
|
||||
input: RepoInputCheckFavorite,
|
||||
): Promise<RepoOutputFavoriteStatus> {
|
||||
const favorite = await prisma.folderFavorite.findUnique({
|
||||
where: {
|
||||
userId_folderId: {
|
||||
userId: input.userId,
|
||||
folderId: input.folderId,
|
||||
},
|
||||
},
|
||||
});
|
||||
const count = await prisma.folderFavorite.count({
|
||||
where: { folderId: input.folderId },
|
||||
});
|
||||
return {
|
||||
isFavorited: !!favorite,
|
||||
favoriteCount: count,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user