feat(api-task): implement task crud with tag filters
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { ConfigModule } from "@nestjs/config";
|
import { ConfigModule } from "@nestjs/config";
|
||||||
import { AuthModule } from "./auth/auth.module";
|
import { AuthModule } from "./auth/auth.module";
|
||||||
|
import { PrismaModule } from "./prisma/prisma.module";
|
||||||
|
import { TaskModule } from "./task/task.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -8,7 +10,9 @@ import { AuthModule } from "./auth/auth.module";
|
|||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
envFilePath: ".env"
|
envFilePath: ".env"
|
||||||
}),
|
}),
|
||||||
AuthModule
|
PrismaModule,
|
||||||
|
AuthModule,
|
||||||
|
TaskModule
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Global, Module } from "@nestjs/common";
|
||||||
|
import { PrismaService } from "./prisma.service";
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [PrismaService],
|
||||||
|
exports: [PrismaService]
|
||||||
|
})
|
||||||
|
export class PrismaModule {}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Injectable, OnModuleDestroy, OnModuleInit } from "@nestjs/common";
|
||||||
|
import { PrismaClient } from "../../generated/prisma/client";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
||||||
|
async onModuleInit(): Promise<void> {
|
||||||
|
await this.$connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleDestroy(): Promise<void> {
|
||||||
|
await this.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { Transform } from "class-transformer";
|
||||||
|
import {
|
||||||
|
IsArray,
|
||||||
|
IsDateString,
|
||||||
|
IsEnum,
|
||||||
|
IsObject,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
MaxLength,
|
||||||
|
MinLength
|
||||||
|
} from "class-validator";
|
||||||
|
import { TaskPriority, TaskStatus } from "../../../generated/prisma/client";
|
||||||
|
|
||||||
|
function normalizeString(value: unknown): unknown {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateTaskDto {
|
||||||
|
@Transform(({ value }) => normalizeString(value))
|
||||||
|
@IsString()
|
||||||
|
@MinLength(1)
|
||||||
|
@MaxLength(120)
|
||||||
|
title!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
contentJson?: Record<string, unknown>;
|
||||||
|
|
||||||
|
@Transform(({ value }) => normalizeString(value))
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(20000)
|
||||||
|
contentText?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(TaskPriority)
|
||||||
|
priority?: TaskPriority;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(TaskStatus)
|
||||||
|
status?: TaskStatus;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
ddl?: string;
|
||||||
|
|
||||||
|
@Transform(({ value }) => {
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.map((item) => normalizeString(item));
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
@MinLength(1, { each: true })
|
||||||
|
@MaxLength(30, { each: true })
|
||||||
|
tagNames?: string[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { Transform, Type } from "class-transformer";
|
||||||
|
import { IsArray, IsEnum, IsInt, IsOptional, IsString, Max, MaxLength, Min } from "class-validator";
|
||||||
|
import { TaskPriority, TaskStatus } from "../../../generated/prisma/client";
|
||||||
|
|
||||||
|
export enum TaskSortBy {
|
||||||
|
CREATED_AT = "createdAt",
|
||||||
|
UPDATED_AT = "updatedAt",
|
||||||
|
DDL = "ddl"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TaskSortOrder {
|
||||||
|
ASC = "asc",
|
||||||
|
DESC = "desc"
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeString(value: unknown): string | undefined {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = value.trim();
|
||||||
|
if (!normalized) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ListTasksQueryDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(TaskStatus)
|
||||||
|
status?: TaskStatus;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(TaskPriority)
|
||||||
|
priority?: TaskPriority;
|
||||||
|
|
||||||
|
@Transform(({ value }) => {
|
||||||
|
if (value === undefined || value === null || value === "") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const normalized = value
|
||||||
|
.map((item) => normalizeString(item))
|
||||||
|
.filter((item): item is string => item !== undefined);
|
||||||
|
return normalized.length > 0 ? normalized : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const normalized = value
|
||||||
|
.split(",")
|
||||||
|
.map((item) => normalizeString(item))
|
||||||
|
.filter((item): item is string => item !== undefined);
|
||||||
|
return normalized.length > 0 ? normalized : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
@MaxLength(30, { each: true })
|
||||||
|
tags?: string[];
|
||||||
|
|
||||||
|
@Transform(({ value }) => normalizeString(value))
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(120)
|
||||||
|
keyword?: string;
|
||||||
|
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
page?: number;
|
||||||
|
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
@Max(100)
|
||||||
|
pageSize?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(TaskSortBy)
|
||||||
|
sortBy?: TaskSortBy;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(TaskSortOrder)
|
||||||
|
sortOrder?: TaskSortOrder;
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { Transform } from "class-transformer";
|
||||||
|
import {
|
||||||
|
IsArray,
|
||||||
|
IsDateString,
|
||||||
|
IsEnum,
|
||||||
|
IsObject,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
MaxLength,
|
||||||
|
MinLength
|
||||||
|
} from "class-validator";
|
||||||
|
import { TaskPriority, TaskStatus } from "../../../generated/prisma/client";
|
||||||
|
|
||||||
|
function normalizeString(value: unknown): unknown {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateTaskDto {
|
||||||
|
@Transform(({ value }) => normalizeString(value))
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MinLength(1)
|
||||||
|
@MaxLength(120)
|
||||||
|
title?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
contentJson?: Record<string, unknown>;
|
||||||
|
|
||||||
|
@Transform(({ value }) => normalizeString(value))
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(20000)
|
||||||
|
contentText?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(TaskPriority)
|
||||||
|
priority?: TaskPriority;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(TaskStatus)
|
||||||
|
status?: TaskStatus;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
ddl?: string;
|
||||||
|
|
||||||
|
@Transform(({ value }) => {
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.map((item) => normalizeString(item));
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
@MinLength(1, { each: true })
|
||||||
|
@MaxLength(30, { each: true })
|
||||||
|
tagNames?: string[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Headers,
|
||||||
|
Param,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
UnauthorizedException
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { CreateTaskDto } from "./dto/create-task.dto";
|
||||||
|
import { ListTasksQueryDto } from "./dto/list-tasks-query.dto";
|
||||||
|
import { UpdateTaskDto } from "./dto/update-task.dto";
|
||||||
|
import { ListTasksResponse, TaskResponse, TaskService } from "./task.service";
|
||||||
|
|
||||||
|
@Controller("tasks")
|
||||||
|
export class TaskController {
|
||||||
|
constructor(private readonly taskService: TaskService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async listTasks(
|
||||||
|
@Headers("x-user-id") userIdHeader: string | string[] | undefined,
|
||||||
|
@Query() query: ListTasksQueryDto
|
||||||
|
): Promise<ListTasksResponse> {
|
||||||
|
return this.taskService.listTasks(this.resolveUserId(userIdHeader), query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(":taskId")
|
||||||
|
async getTaskById(
|
||||||
|
@Headers("x-user-id") userIdHeader: string | string[] | undefined,
|
||||||
|
@Param("taskId") taskId: string
|
||||||
|
): Promise<TaskResponse> {
|
||||||
|
return this.taskService.getTaskById(this.resolveUserId(userIdHeader), taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
async createTask(
|
||||||
|
@Headers("x-user-id") userIdHeader: string | string[] | undefined,
|
||||||
|
@Body() body: CreateTaskDto
|
||||||
|
): Promise<TaskResponse> {
|
||||||
|
return this.taskService.createTask(this.resolveUserId(userIdHeader), body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(":taskId")
|
||||||
|
async updateTask(
|
||||||
|
@Headers("x-user-id") userIdHeader: string | string[] | undefined,
|
||||||
|
@Param("taskId") taskId: string,
|
||||||
|
@Body() body: UpdateTaskDto
|
||||||
|
): Promise<TaskResponse> {
|
||||||
|
return this.taskService.updateTask(this.resolveUserId(userIdHeader), taskId, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(":taskId")
|
||||||
|
async deleteTask(
|
||||||
|
@Headers("x-user-id") userIdHeader: string | string[] | undefined,
|
||||||
|
@Param("taskId") taskId: string
|
||||||
|
): Promise<{ success: boolean }> {
|
||||||
|
return this.taskService.deleteTask(this.resolveUserId(userIdHeader), taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveUserId(userIdHeader: string | string[] | undefined): string {
|
||||||
|
const userId = Array.isArray(userIdHeader) ? userIdHeader[0] : userIdHeader;
|
||||||
|
if (!userId) {
|
||||||
|
throw new UnauthorizedException("缺少用户上下文");
|
||||||
|
}
|
||||||
|
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { PrismaModule } from "../prisma/prisma.module";
|
||||||
|
import { TaskController } from "./task.controller";
|
||||||
|
import { TaskService } from "./task.service";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule],
|
||||||
|
controllers: [TaskController],
|
||||||
|
providers: [TaskService]
|
||||||
|
})
|
||||||
|
export class TaskModule {}
|
||||||
@@ -0,0 +1,390 @@
|
|||||||
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
import { Prisma, TaskPriority, TaskStatus } from "../../generated/prisma/client";
|
||||||
|
import { PrismaService } from "../prisma/prisma.service";
|
||||||
|
import { CreateTaskDto } from "./dto/create-task.dto";
|
||||||
|
import { ListTasksQueryDto, TaskSortBy, TaskSortOrder } from "./dto/list-tasks-query.dto";
|
||||||
|
import { UpdateTaskDto } from "./dto/update-task.dto";
|
||||||
|
|
||||||
|
type TaskEntity = Prisma.TaskGetPayload<{
|
||||||
|
include: {
|
||||||
|
taskTags: {
|
||||||
|
include: {
|
||||||
|
tag: {
|
||||||
|
select: {
|
||||||
|
name: true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type TaskResponse = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
contentJson: unknown | null;
|
||||||
|
contentText: string | null;
|
||||||
|
priority: TaskPriority;
|
||||||
|
status: TaskStatus;
|
||||||
|
ddl: string | null;
|
||||||
|
completedAt: string | null;
|
||||||
|
version: number;
|
||||||
|
tags: string[];
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ListTasksResponse = {
|
||||||
|
items: TaskResponse[];
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TaskService {
|
||||||
|
constructor(private readonly prismaService: PrismaService) {}
|
||||||
|
|
||||||
|
async listTasks(userId: string, query: ListTasksQueryDto): Promise<ListTasksResponse> {
|
||||||
|
const page = query.page ?? 1;
|
||||||
|
const pageSize = query.pageSize ?? 20;
|
||||||
|
const skip = (page - 1) * pageSize;
|
||||||
|
|
||||||
|
const where = this.buildWhereInput(userId, query);
|
||||||
|
const orderBy = this.buildOrderByInput(query);
|
||||||
|
|
||||||
|
const [items, total] = await Promise.all([
|
||||||
|
this.prismaService.task.findMany({
|
||||||
|
where,
|
||||||
|
orderBy,
|
||||||
|
skip,
|
||||||
|
take: pageSize,
|
||||||
|
include: {
|
||||||
|
taskTags: {
|
||||||
|
include: {
|
||||||
|
tag: {
|
||||||
|
select: {
|
||||||
|
name: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
this.prismaService.task.count({ where })
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: items.map((item) => this.serializeTask(item)),
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
total
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTaskById(userId: string, taskId: string): Promise<TaskResponse> {
|
||||||
|
const task = await this.prismaService.task.findFirst({
|
||||||
|
where: {
|
||||||
|
id: taskId,
|
||||||
|
userId
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
taskTags: {
|
||||||
|
include: {
|
||||||
|
tag: {
|
||||||
|
select: {
|
||||||
|
name: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!task) {
|
||||||
|
throw new NotFoundException("任务不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.serializeTask(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createTask(userId: string, body: CreateTaskDto): Promise<TaskResponse> {
|
||||||
|
const tagNames = this.normalizeTagNames(body.tagNames);
|
||||||
|
const nextStatus = body.status ?? TaskStatus.TODO;
|
||||||
|
const contentJson =
|
||||||
|
body.contentJson !== undefined ? (body.contentJson as Prisma.InputJsonValue) : undefined;
|
||||||
|
|
||||||
|
const task = await this.prismaService.$transaction(async (tx) => {
|
||||||
|
const createdTask = await tx.task.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
title: body.title,
|
||||||
|
contentJson,
|
||||||
|
contentText: body.contentText ?? null,
|
||||||
|
priority: body.priority ?? TaskPriority.MEDIUM,
|
||||||
|
status: nextStatus,
|
||||||
|
ddl: body.ddl ? new Date(body.ddl) : null,
|
||||||
|
completedAt: nextStatus === TaskStatus.DONE ? new Date() : null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.replaceTaskTags(tx, userId, createdTask.id, tagNames);
|
||||||
|
|
||||||
|
return tx.task.findUniqueOrThrow({
|
||||||
|
where: { id: createdTask.id },
|
||||||
|
include: {
|
||||||
|
taskTags: {
|
||||||
|
include: {
|
||||||
|
tag: {
|
||||||
|
select: {
|
||||||
|
name: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.serializeTask(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTask(userId: string, taskId: string, body: UpdateTaskDto): Promise<TaskResponse> {
|
||||||
|
const currentTask = await this.prismaService.task.findFirst({
|
||||||
|
where: {
|
||||||
|
id: taskId,
|
||||||
|
userId
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
status: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!currentTask) {
|
||||||
|
throw new NotFoundException("任务不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: Prisma.TaskUpdateInput = {
|
||||||
|
version: {
|
||||||
|
increment: 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (body.title !== undefined) {
|
||||||
|
data.title = body.title;
|
||||||
|
}
|
||||||
|
if (body.contentJson !== undefined) {
|
||||||
|
data.contentJson = body.contentJson as Prisma.InputJsonValue;
|
||||||
|
}
|
||||||
|
if (body.contentText !== undefined) {
|
||||||
|
data.contentText = body.contentText;
|
||||||
|
}
|
||||||
|
if (body.priority !== undefined) {
|
||||||
|
data.priority = body.priority;
|
||||||
|
}
|
||||||
|
if (body.status !== undefined) {
|
||||||
|
data.status = body.status;
|
||||||
|
if (body.status === TaskStatus.DONE && currentTask.status !== TaskStatus.DONE) {
|
||||||
|
data.completedAt = new Date();
|
||||||
|
} else if (body.status !== TaskStatus.DONE) {
|
||||||
|
data.completedAt = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (body.ddl !== undefined) {
|
||||||
|
data.ddl = body.ddl ? new Date(body.ddl) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldReplaceTags = body.tagNames !== undefined;
|
||||||
|
const nextTagNames = this.normalizeTagNames(body.tagNames);
|
||||||
|
|
||||||
|
const task = await this.prismaService.$transaction(async (tx) => {
|
||||||
|
await tx.task.update({
|
||||||
|
where: { id: taskId },
|
||||||
|
data
|
||||||
|
});
|
||||||
|
|
||||||
|
if (shouldReplaceTags) {
|
||||||
|
await this.replaceTaskTags(tx, userId, taskId, nextTagNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.task.findUniqueOrThrow({
|
||||||
|
where: { id: taskId },
|
||||||
|
include: {
|
||||||
|
taskTags: {
|
||||||
|
include: {
|
||||||
|
tag: {
|
||||||
|
select: {
|
||||||
|
name: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.serializeTask(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteTask(userId: string, taskId: string): Promise<{ success: boolean }> {
|
||||||
|
const deleted = await this.prismaService.task.deleteMany({
|
||||||
|
where: {
|
||||||
|
id: taskId,
|
||||||
|
userId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (deleted.count === 0) {
|
||||||
|
throw new NotFoundException("任务不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildWhereInput(userId: string, query: ListTasksQueryDto): Prisma.TaskWhereInput {
|
||||||
|
const where: Prisma.TaskWhereInput = {
|
||||||
|
userId
|
||||||
|
};
|
||||||
|
|
||||||
|
if (query.status !== undefined) {
|
||||||
|
where.status = query.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.priority !== undefined) {
|
||||||
|
where.priority = query.priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.tags !== undefined && query.tags.length > 0) {
|
||||||
|
where.taskTags = {
|
||||||
|
some: {
|
||||||
|
tag: {
|
||||||
|
name: {
|
||||||
|
in: query.tags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.keyword !== undefined && query.keyword.length > 0) {
|
||||||
|
where.OR = [
|
||||||
|
{
|
||||||
|
title: {
|
||||||
|
contains: query.keyword,
|
||||||
|
mode: "insensitive"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
contentText: {
|
||||||
|
contains: query.keyword,
|
||||||
|
mode: "insensitive"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return where;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildOrderByInput(query: ListTasksQueryDto): Prisma.TaskOrderByWithRelationInput {
|
||||||
|
const order: Prisma.SortOrder =
|
||||||
|
query.sortOrder === TaskSortOrder.ASC ? Prisma.SortOrder.asc : Prisma.SortOrder.desc;
|
||||||
|
|
||||||
|
if (query.sortBy === TaskSortBy.CREATED_AT) {
|
||||||
|
return { createdAt: order };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.sortBy === TaskSortBy.DDL) {
|
||||||
|
return { ddl: order };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { updatedAt: order };
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeTagNames(tagNames: string[] | undefined): string[] {
|
||||||
|
if (!tagNames) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: string[] = [];
|
||||||
|
const uniqueNames = new Set<string>();
|
||||||
|
|
||||||
|
for (const rawTagName of tagNames) {
|
||||||
|
const normalized = rawTagName.trim();
|
||||||
|
if (!normalized) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueKey = normalized.toLocaleLowerCase();
|
||||||
|
if (uniqueNames.has(uniqueKey)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
uniqueNames.add(uniqueKey);
|
||||||
|
result.push(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async replaceTaskTags(
|
||||||
|
tx: Prisma.TransactionClient,
|
||||||
|
userId: string,
|
||||||
|
taskId: string,
|
||||||
|
tagNames: string[]
|
||||||
|
): Promise<void> {
|
||||||
|
await tx.taskTag.deleteMany({
|
||||||
|
where: {
|
||||||
|
taskId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tagNames.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = await Promise.all(
|
||||||
|
tagNames.map((name) =>
|
||||||
|
tx.tag.upsert({
|
||||||
|
where: {
|
||||||
|
userId_name: {
|
||||||
|
userId,
|
||||||
|
name
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
userId,
|
||||||
|
name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await tx.taskTag.createMany({
|
||||||
|
data: tags.map((tag) => ({
|
||||||
|
taskId,
|
||||||
|
tagId: tag.id
|
||||||
|
})),
|
||||||
|
skipDuplicates: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private serializeTask(task: TaskEntity): TaskResponse {
|
||||||
|
return {
|
||||||
|
id: task.id,
|
||||||
|
title: task.title,
|
||||||
|
contentJson: task.contentJson,
|
||||||
|
contentText: task.contentText,
|
||||||
|
priority: task.priority,
|
||||||
|
status: task.status,
|
||||||
|
ddl: task.ddl?.toISOString() ?? null,
|
||||||
|
completedAt: task.completedAt?.toISOString() ?? null,
|
||||||
|
version: task.version,
|
||||||
|
tags: task.taskTags.map((taskTag) => taskTag.tag.name),
|
||||||
|
createdAt: task.createdAt.toISOString(),
|
||||||
|
updatedAt: task.updatedAt.toISOString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
"extends": "../../packages/tsconfig/nest-app.json",
|
"extends": "../../packages/tsconfig/nest-app.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"rootDir": "src",
|
"rootDir": ".",
|
||||||
"outDir": "dist"
|
"outDir": "dist"
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"],
|
"include": ["src/**/*.ts", "generated/prisma/**/*.ts"],
|
||||||
"exclude": ["dist", "node_modules"]
|
"exclude": ["dist", "node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user