Initial commit
This commit is contained in:
171
api/modules/todo/__tests__/parser.test.ts
Normal file
171
api/modules/todo/__tests__/parser.test.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { parseTodoContent, generateTodoContent } from '../parser.js'
|
||||
import type { DayTodo } from '../types.js'
|
||||
|
||||
describe('parseTodoContent', () => {
|
||||
it('should parse basic todo content correctly', () => {
|
||||
const content = `## 2024-01-01
|
||||
- √ 完成工作
|
||||
- ○ 购物`
|
||||
|
||||
const result = parseTodoContent(content)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].date).toBe('2024-01-01')
|
||||
expect(result[0].items).toHaveLength(2)
|
||||
expect(result[0].items[0].content).toBe('完成工作')
|
||||
expect(result[0].items[1].content).toBe('购物')
|
||||
})
|
||||
|
||||
it('should correctly identify completed status with √', () => {
|
||||
const content = `## 2024-01-01
|
||||
- √ 已完成任务`
|
||||
|
||||
const result = parseTodoContent(content)
|
||||
|
||||
expect(result[0].items[0].completed).toBe(true)
|
||||
})
|
||||
|
||||
it('should correctly identify incomplete status with ○', () => {
|
||||
const content = `## 2024-01-01
|
||||
- ○ 未完成任务`
|
||||
|
||||
const result = parseTodoContent(content)
|
||||
|
||||
expect(result[0].items[0].completed).toBe(false)
|
||||
})
|
||||
|
||||
it('should parse multiple days correctly', () => {
|
||||
const content = `## 2024-01-01
|
||||
- √ 第一天任务
|
||||
|
||||
## 2024-01-02
|
||||
- ○ 第二天任务
|
||||
|
||||
## 2024-01-03
|
||||
- √ 第三天任务`
|
||||
|
||||
const result = parseTodoContent(content)
|
||||
|
||||
expect(result).toHaveLength(3)
|
||||
expect(result[0].date).toBe('2024-01-01')
|
||||
expect(result[1].date).toBe('2024-01-02')
|
||||
expect(result[2].date).toBe('2024-01-03')
|
||||
expect(result[0].items[0].content).toBe('第一天任务')
|
||||
expect(result[1].items[0].content).toBe('第二天任务')
|
||||
expect(result[2].items[0].content).toBe('第三天任务')
|
||||
})
|
||||
|
||||
it('should handle empty content', () => {
|
||||
const content = ''
|
||||
|
||||
const result = parseTodoContent(content)
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should ignore invalid format lines', () => {
|
||||
const content = `## 2024-01-01
|
||||
这是一行普通文本
|
||||
- 无效格式
|
||||
- x 错误的标记
|
||||
random line
|
||||
- √ 有效的任务`
|
||||
|
||||
const result = parseTodoContent(content)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].items).toHaveLength(1)
|
||||
expect(result[0].items[0].content).toBe('有效的任务')
|
||||
})
|
||||
|
||||
it('should generate unique IDs for items', () => {
|
||||
const content = `## 2024-01-01
|
||||
- √ 任务一
|
||||
- ○ 任务二
|
||||
- √ 任务三`
|
||||
|
||||
const result = parseTodoContent(content)
|
||||
|
||||
expect(result[0].items[0].id).toBe('2024-01-01-0')
|
||||
expect(result[0].items[1].id).toBe('2024-01-01-1')
|
||||
expect(result[0].items[2].id).toBe('2024-01-01-2')
|
||||
})
|
||||
})
|
||||
|
||||
describe('generateTodoContent', () => {
|
||||
it('should generate basic todo content correctly', () => {
|
||||
const dayTodos: DayTodo[] = [
|
||||
{
|
||||
date: '2024-01-01',
|
||||
items: [
|
||||
{ id: '2024-01-01-0', content: '完成工作', completed: true },
|
||||
{ id: '2024-01-01-1', content: '购物', completed: false }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const result = generateTodoContent(dayTodos)
|
||||
|
||||
expect(result).toBe(`## 2024-01-01
|
||||
- √ 完成工作
|
||||
- ○ 购物`)
|
||||
})
|
||||
|
||||
it('should include completed status in generated content', () => {
|
||||
const dayTodos: DayTodo[] = [
|
||||
{
|
||||
date: '2024-01-01',
|
||||
items: [
|
||||
{ id: '2024-01-01-0', content: '已完成', completed: true },
|
||||
{ id: '2024-01-01-1', content: '未完成', completed: false }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const result = generateTodoContent(dayTodos)
|
||||
|
||||
expect(result).toContain('√ 已完成')
|
||||
expect(result).toContain('○ 未完成')
|
||||
})
|
||||
|
||||
it('should sort dates in ascending order', () => {
|
||||
const dayTodos: DayTodo[] = [
|
||||
{ date: '2024-01-03', items: [{ id: '1', content: '第三天', completed: false }] },
|
||||
{ date: '2024-01-01', items: [{ id: '2', content: '第一天', completed: false }] },
|
||||
{ date: '2024-01-02', items: [{ id: '3', content: '第二天', completed: false }] }
|
||||
]
|
||||
|
||||
const result = generateTodoContent(dayTodos)
|
||||
|
||||
const firstDateIndex = result.indexOf('2024-01-01')
|
||||
const secondDateIndex = result.indexOf('2024-01-02')
|
||||
const thirdDateIndex = result.indexOf('2024-01-03')
|
||||
|
||||
expect(firstDateIndex).toBeLessThan(secondDateIndex)
|
||||
expect(secondDateIndex).toBeLessThan(thirdDateIndex)
|
||||
})
|
||||
|
||||
it('should handle empty array', () => {
|
||||
const dayTodos: DayTodo[] = []
|
||||
|
||||
const result = generateTodoContent(dayTodos)
|
||||
|
||||
expect(result).toBe('')
|
||||
})
|
||||
|
||||
it('should generate content for multiple days with sorting', () => {
|
||||
const dayTodos: DayTodo[] = [
|
||||
{ date: '2024-01-02', items: [{ id: '1', content: '第二天', completed: true }] },
|
||||
{ date: '2024-01-01', items: [{ id: '2', content: '第一天', completed: false }] }
|
||||
]
|
||||
|
||||
const result = generateTodoContent(dayTodos)
|
||||
|
||||
expect(result).toBe(`## 2024-01-01
|
||||
- ○ 第一天
|
||||
|
||||
## 2024-01-02
|
||||
- √ 第二天`)
|
||||
})
|
||||
})
|
||||
28
api/modules/todo/index.ts
Normal file
28
api/modules/todo/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { Router } from 'express'
|
||||
import type { ServiceContainer } from '../../infra/container.js'
|
||||
import { createApiModule } from '../../infra/createModule.js'
|
||||
import { TODO_MODULE } from '../../../shared/modules/todo/index.js'
|
||||
import { TodoService } from './service.js'
|
||||
import { createTodoRoutes } from './routes.js'
|
||||
|
||||
export * from './types.js'
|
||||
export * from './parser.js'
|
||||
export * from './service.js'
|
||||
export * from './schemas.js'
|
||||
export * from './routes.js'
|
||||
|
||||
export const createTodoModule = () => {
|
||||
return createApiModule(TODO_MODULE, {
|
||||
routes: (container: ServiceContainer): Router => {
|
||||
const todoService = container.getSync<TodoService>('todoService')
|
||||
return createTodoRoutes({ todoService })
|
||||
},
|
||||
lifecycle: {
|
||||
onLoad: (container: ServiceContainer): void => {
|
||||
container.register('todoService', () => new TodoService())
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default createTodoModule
|
||||
51
api/modules/todo/parser.ts
Normal file
51
api/modules/todo/parser.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { TodoItem, DayTodo } from './types.js'
|
||||
|
||||
export const parseTodoContent = (content: string): DayTodo[] => {
|
||||
const lines = content.split('\n')
|
||||
const result: DayTodo[] = []
|
||||
let currentDate: string | null = null
|
||||
let currentItems: TodoItem[] = []
|
||||
let itemId = 0
|
||||
|
||||
for (const line of lines) {
|
||||
const dateMatch = line.match(/^##\s*(\d{4}-\d{2}-\d{2})/)
|
||||
if (dateMatch) {
|
||||
if (currentDate) {
|
||||
result.push({ date: currentDate, items: currentItems })
|
||||
}
|
||||
currentDate = dateMatch[1]
|
||||
currentItems = []
|
||||
} else if (currentDate) {
|
||||
const todoMatch = line.match(/^- (√|○) (.*)$/)
|
||||
if (todoMatch) {
|
||||
currentItems.push({
|
||||
id: `${currentDate}-${itemId++}`,
|
||||
content: todoMatch[2],
|
||||
completed: todoMatch[1] === '√'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentDate) {
|
||||
result.push({ date: currentDate, items: currentItems })
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export const generateTodoContent = (dayTodos: DayTodo[]): string => {
|
||||
const lines: string[] = []
|
||||
const sortedDays = [...dayTodos].sort((a, b) => a.date.localeCompare(b.date))
|
||||
|
||||
for (const day of sortedDays) {
|
||||
lines.push(`## ${day.date}`)
|
||||
for (const item of day.items) {
|
||||
const checkbox = item.completed ? '√' : '○'
|
||||
lines.push(`- ${checkbox} ${item.content}`)
|
||||
}
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
return lines.join('\n').trimEnd()
|
||||
}
|
||||
99
api/modules/todo/routes.ts
Normal file
99
api/modules/todo/routes.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import express, { type Request, type Response, type Router } from 'express'
|
||||
import { asyncHandler } from '../../utils/asyncHandler.js'
|
||||
import { successResponse } from '../../utils/response.js'
|
||||
import { validateBody, validateQuery } from '../../middlewares/validate.js'
|
||||
import { TodoService } from './service.js'
|
||||
import {
|
||||
getTodoQuerySchema,
|
||||
saveTodoSchema,
|
||||
addTodoSchema,
|
||||
toggleTodoSchema,
|
||||
updateTodoSchema,
|
||||
deleteTodoSchema,
|
||||
} from './schemas.js'
|
||||
|
||||
export interface TodoRoutesDependencies {
|
||||
todoService: TodoService
|
||||
}
|
||||
|
||||
export const createTodoRoutes = (deps: TodoRoutesDependencies): Router => {
|
||||
const router = express.Router()
|
||||
const { todoService } = deps
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
validateQuery(getTodoQuerySchema),
|
||||
asyncHandler(async (req: Request, res: Response) => {
|
||||
const year = parseInt(req.query.year as string) || new Date().getFullYear()
|
||||
const month = parseInt(req.query.month as string) || new Date().getMonth() + 1
|
||||
|
||||
const result = await todoService.getTodo(year, month)
|
||||
successResponse(res, result)
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/save',
|
||||
validateBody(saveTodoSchema),
|
||||
asyncHandler(async (req: Request, res: Response) => {
|
||||
const { year, month, dayTodos } = req.body
|
||||
|
||||
await todoService.saveTodo(year, month, dayTodos)
|
||||
|
||||
successResponse(res, null)
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/add',
|
||||
validateBody(addTodoSchema),
|
||||
asyncHandler(async (req: Request, res: Response) => {
|
||||
const { year, month, date, content: todoContent } = req.body
|
||||
|
||||
const dayTodos = await todoService.addTodo(year, month, date, todoContent)
|
||||
|
||||
successResponse(res, { dayTodos })
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/toggle',
|
||||
validateBody(toggleTodoSchema),
|
||||
asyncHandler(async (req: Request, res: Response) => {
|
||||
const { year, month, date, itemIndex, completed } = req.body
|
||||
|
||||
const dayTodos = await todoService.toggleTodo(year, month, date, itemIndex, completed)
|
||||
|
||||
successResponse(res, { dayTodos })
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/update',
|
||||
validateBody(updateTodoSchema),
|
||||
asyncHandler(async (req: Request, res: Response) => {
|
||||
const { year, month, date, itemIndex, content: newContent } = req.body
|
||||
|
||||
const dayTodos = await todoService.updateTodo(year, month, date, itemIndex, newContent)
|
||||
|
||||
successResponse(res, { dayTodos })
|
||||
}),
|
||||
)
|
||||
|
||||
router.delete(
|
||||
'/delete',
|
||||
validateBody(deleteTodoSchema),
|
||||
asyncHandler(async (req: Request, res: Response) => {
|
||||
const { year, month, date, itemIndex } = req.body
|
||||
|
||||
const dayTodos = await todoService.deleteTodo(year, month, date, itemIndex)
|
||||
|
||||
successResponse(res, { dayTodos })
|
||||
}),
|
||||
)
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
const todoService = new TodoService()
|
||||
export default createTodoRoutes({ todoService })
|
||||
53
api/modules/todo/schemas.ts
Normal file
53
api/modules/todo/schemas.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const todoItemSchema = z.object({
|
||||
id: z.string(),
|
||||
content: z.string(),
|
||||
completed: z.boolean(),
|
||||
})
|
||||
|
||||
const dayTodoSchema = z.object({
|
||||
date: z.string(),
|
||||
items: z.array(todoItemSchema),
|
||||
})
|
||||
|
||||
export const getTodoQuerySchema = z.object({
|
||||
year: z.string().optional(),
|
||||
month: z.string().optional(),
|
||||
})
|
||||
|
||||
export const saveTodoSchema = z.object({
|
||||
year: z.number().int().positive(),
|
||||
month: z.number().int().min(1).max(12),
|
||||
dayTodos: z.array(dayTodoSchema),
|
||||
})
|
||||
|
||||
export const addTodoSchema = z.object({
|
||||
year: z.number().int().positive(),
|
||||
month: z.number().int().min(1).max(12),
|
||||
date: z.string(),
|
||||
content: z.string(),
|
||||
})
|
||||
|
||||
export const toggleTodoSchema = z.object({
|
||||
year: z.number().int().positive(),
|
||||
month: z.number().int().min(1).max(12),
|
||||
date: z.string(),
|
||||
itemIndex: z.number().int().nonnegative(),
|
||||
completed: z.boolean(),
|
||||
})
|
||||
|
||||
export const updateTodoSchema = z.object({
|
||||
year: z.number().int().positive(),
|
||||
month: z.number().int().min(1).max(12),
|
||||
date: z.string(),
|
||||
itemIndex: z.number().int().nonnegative(),
|
||||
content: z.string(),
|
||||
})
|
||||
|
||||
export const deleteTodoSchema = z.object({
|
||||
year: z.number().int().positive(),
|
||||
month: z.number().int().min(1).max(12),
|
||||
date: z.string(),
|
||||
itemIndex: z.number().int().nonnegative(),
|
||||
})
|
||||
216
api/modules/todo/service.ts
Normal file
216
api/modules/todo/service.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { resolveNotebookPath } from '../../utils/pathSafety.js'
|
||||
import { NotFoundError } from '../../../shared/errors/index.js'
|
||||
import { parseTodoContent, generateTodoContent } from './parser.js'
|
||||
import type { DayTodo, TodoFilePath, ParsedTodoFile, GetTodoResult } from './types.js'
|
||||
|
||||
export interface TodoServiceDependencies {
|
||||
}
|
||||
|
||||
export class TodoService {
|
||||
constructor(private deps: TodoServiceDependencies = {}) {}
|
||||
|
||||
getTodoFilePath(year: number, month: number): TodoFilePath {
|
||||
const yearStr = year.toString()
|
||||
const monthStr = month.toString().padStart(2, '0')
|
||||
const relPath = `TODO/${yearStr}/${yearStr}${monthStr}TODO.md`
|
||||
const { fullPath } = resolveNotebookPath(relPath)
|
||||
return { relPath, fullPath }
|
||||
}
|
||||
|
||||
async ensureTodoFileExists(fullPath: string): Promise<void> {
|
||||
const dir = path.dirname(fullPath)
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
try {
|
||||
await fs.access(fullPath)
|
||||
} catch {
|
||||
await fs.writeFile(fullPath, '', 'utf-8')
|
||||
}
|
||||
}
|
||||
|
||||
async loadAndParseTodoFile(year: number, month: number): Promise<ParsedTodoFile> {
|
||||
const { fullPath } = this.getTodoFilePath(year, month)
|
||||
try {
|
||||
await fs.access(fullPath)
|
||||
} catch {
|
||||
throw new NotFoundError('TODO file not found')
|
||||
}
|
||||
const content = await fs.readFile(fullPath, 'utf-8')
|
||||
return { fullPath, dayTodos: parseTodoContent(content) }
|
||||
}
|
||||
|
||||
async saveTodoFile(fullPath: string, dayTodos: DayTodo[]): Promise<void> {
|
||||
const content = generateTodoContent(dayTodos)
|
||||
await fs.writeFile(fullPath, content, 'utf-8')
|
||||
}
|
||||
|
||||
async getTodo(year: number, month: number): Promise<GetTodoResult> {
|
||||
const { fullPath } = this.getTodoFilePath(year, month)
|
||||
|
||||
let dayTodos: DayTodo[] = []
|
||||
try {
|
||||
await fs.access(fullPath)
|
||||
const content = await fs.readFile(fullPath, 'utf-8')
|
||||
dayTodos = parseTodoContent(content)
|
||||
} catch {
|
||||
// 文件不存在
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const todayStr = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}`
|
||||
|
||||
const yesterday = new Date(now)
|
||||
yesterday.setDate(yesterday.getDate() - 1)
|
||||
const yesterdayStr = `${yesterday.getFullYear()}-${(yesterday.getMonth() + 1).toString().padStart(2, '0')}-${yesterday.getDate().toString().padStart(2, '0')}`
|
||||
|
||||
if (year === now.getFullYear() && month === now.getMonth() + 1) {
|
||||
const migrated = this.migrateIncompleteItems(dayTodos, todayStr, yesterdayStr)
|
||||
|
||||
if (migrated) {
|
||||
const newContent = generateTodoContent(dayTodos)
|
||||
await this.ensureTodoFileExists(fullPath)
|
||||
await fs.writeFile(fullPath, newContent, 'utf-8')
|
||||
}
|
||||
}
|
||||
|
||||
return { dayTodos, year, month }
|
||||
}
|
||||
|
||||
private migrateIncompleteItems(dayTodos: DayTodo[], todayStr: string, yesterdayStr: string): boolean {
|
||||
let migrated = false
|
||||
|
||||
const yesterdayTodo = dayTodos.find(d => d.date === yesterdayStr)
|
||||
if (yesterdayTodo) {
|
||||
const incompleteItems = yesterdayTodo.items.filter(item => !item.completed)
|
||||
if (incompleteItems.length > 0) {
|
||||
const todayTodo = dayTodos.find(d => d.date === todayStr)
|
||||
if (todayTodo) {
|
||||
const existingIds = new Set(todayTodo.items.map(i => i.id))
|
||||
const itemsToAdd = incompleteItems.map((item, idx) => ({
|
||||
...item,
|
||||
id: existingIds.has(item.id) ? `${todayStr}-migrated-${idx}` : item.id
|
||||
}))
|
||||
todayTodo.items = [...itemsToAdd, ...todayTodo.items]
|
||||
} else {
|
||||
dayTodos.push({
|
||||
date: todayStr,
|
||||
items: incompleteItems.map((item, idx) => ({
|
||||
...item,
|
||||
id: `${todayStr}-migrated-${idx}`
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
yesterdayTodo.items = yesterdayTodo.items.filter(item => item.completed)
|
||||
if (yesterdayTodo.items.length === 0) {
|
||||
const index = dayTodos.findIndex(d => d.date === yesterdayStr)
|
||||
if (index !== -1) {
|
||||
dayTodos.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
migrated = true
|
||||
}
|
||||
}
|
||||
|
||||
return migrated
|
||||
}
|
||||
|
||||
async saveTodo(year: number, month: number, dayTodos: DayTodo[]): Promise<void> {
|
||||
const { fullPath } = this.getTodoFilePath(year, month)
|
||||
await this.ensureTodoFileExists(fullPath)
|
||||
const content = generateTodoContent(dayTodos)
|
||||
await fs.writeFile(fullPath, content, 'utf-8')
|
||||
}
|
||||
|
||||
async addTodo(year: number, month: number, date: string, todoContent: string): Promise<DayTodo[]> {
|
||||
const { fullPath } = this.getTodoFilePath(year, month)
|
||||
await this.ensureTodoFileExists(fullPath)
|
||||
|
||||
let fileContent = await fs.readFile(fullPath, 'utf-8')
|
||||
const dayTodos = parseTodoContent(fileContent)
|
||||
|
||||
const existingDay = dayTodos.find(d => d.date === date)
|
||||
if (existingDay) {
|
||||
const newId = `${date}-${existingDay.items.length}`
|
||||
existingDay.items.push({
|
||||
id: newId,
|
||||
content: todoContent,
|
||||
completed: false
|
||||
})
|
||||
} else {
|
||||
dayTodos.push({
|
||||
date,
|
||||
items: [{
|
||||
id: `${date}-0`,
|
||||
content: todoContent,
|
||||
completed: false
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
fileContent = generateTodoContent(dayTodos)
|
||||
await fs.writeFile(fullPath, fileContent, 'utf-8')
|
||||
|
||||
return dayTodos
|
||||
}
|
||||
|
||||
async toggleTodo(year: number, month: number, date: string, itemIndex: number, completed: boolean): Promise<DayTodo[]> {
|
||||
const { fullPath, dayTodos } = await this.loadAndParseTodoFile(year, month)
|
||||
|
||||
const day = dayTodos.find(d => d.date === date)
|
||||
if (!day || itemIndex >= day.items.length) {
|
||||
throw new NotFoundError('TODO item not found')
|
||||
}
|
||||
|
||||
day.items[itemIndex].completed = completed
|
||||
|
||||
await this.saveTodoFile(fullPath, dayTodos)
|
||||
|
||||
return dayTodos
|
||||
}
|
||||
|
||||
async updateTodo(year: number, month: number, date: string, itemIndex: number, newContent: string): Promise<DayTodo[]> {
|
||||
const { fullPath, dayTodos } = await this.loadAndParseTodoFile(year, month)
|
||||
|
||||
const day = dayTodos.find(d => d.date === date)
|
||||
if (!day || itemIndex >= day.items.length) {
|
||||
throw new NotFoundError('TODO item not found')
|
||||
}
|
||||
|
||||
day.items[itemIndex].content = newContent
|
||||
|
||||
await this.saveTodoFile(fullPath, dayTodos)
|
||||
|
||||
return dayTodos
|
||||
}
|
||||
|
||||
async deleteTodo(year: number, month: number, date: string, itemIndex: number): Promise<DayTodo[]> {
|
||||
const { fullPath, dayTodos } = await this.loadAndParseTodoFile(year, month)
|
||||
|
||||
const dayIndex = dayTodos.findIndex(d => d.date === date)
|
||||
if (dayIndex === -1) {
|
||||
throw new NotFoundError('Day not found')
|
||||
}
|
||||
|
||||
const day = dayTodos[dayIndex]
|
||||
if (itemIndex >= day.items.length) {
|
||||
throw new NotFoundError('TODO item not found')
|
||||
}
|
||||
|
||||
day.items.splice(itemIndex, 1)
|
||||
|
||||
if (day.items.length === 0) {
|
||||
dayTodos.splice(dayIndex, 1)
|
||||
}
|
||||
|
||||
await this.saveTodoFile(fullPath, dayTodos)
|
||||
|
||||
return dayTodos
|
||||
}
|
||||
}
|
||||
|
||||
export const createTodoService = (deps?: TodoServiceDependencies): TodoService => {
|
||||
return new TodoService(deps)
|
||||
}
|
||||
7
api/modules/todo/types.ts
Normal file
7
api/modules/todo/types.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export { type TodoItem, type DayTodo } from '../../../shared/types/todo.js'
|
||||
export {
|
||||
type TodoFilePath,
|
||||
type ParsedTodoFile,
|
||||
type GetTodoResult,
|
||||
type MigrationContext,
|
||||
} from '../../../shared/modules/todo/types.js'
|
||||
Reference in New Issue
Block a user