@lark-apaas/coding-templates 0.1.19 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/coding-templates",
3
- "version": "0.1.19",
3
+ "version": "0.1.23",
4
4
  "description": "OpenClaw project templates for mclaw CLI",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -23,20 +23,20 @@
23
23
  │ ├── index.css # 全局样式 + 主题变量
24
24
  │ ├── api/ # API 请求封装
25
25
  │ │ └── index.ts
26
- │ ├── components/ # 全局共享组件
26
+ │ ├── components/ # 基础 UI 组件(禁止存放业务组件)
27
27
  │ │ └── ui/ # shadcn/ui 内置组件(勿手动修改)
28
28
  │ ├── pages/ # 页面模块(每个页面一个目录)
29
- │ │ ├── home/
30
- │ │ └── todos/ # CRUD 示例
31
- │ │ ├── index.tsx
32
- │ │ └── components/
29
+ │ │ ├── HomePage/ # 占位示例页,开发时替换为业务首页
30
+ │ │ └── HomePage.tsx # 页面入口文件与目录同名
31
+ │ │ │ └── components/ # 页面专属组件
32
+ │ │ └── NotFoundPage/
33
+ │ │ └── NotFoundPage.tsx
33
34
  │ ├── hooks/ # 自定义 Hooks
34
35
  │ └── lib/ # 工具函数(cn() 等)
35
36
  ├── server/ # 后端代码
36
37
  │ ├── index.ts # Express 入口(dev: Vite HMR 中间件)
37
38
  │ ├── routes/ # API 路由
38
- │ │ ├── index.ts # 路由注册
39
- │ │ └── todos.ts # Todos CRUD 路由
39
+ │ │ └── index.ts # 路由注册
40
40
  │ └── db/ # 数据库层
41
41
  │ ├── schema.ts # Drizzle schema 定义(可由工具生成)
42
42
  │ └── index.ts # 数据库连接
@@ -47,16 +47,6 @@
47
47
 
48
48
  ---
49
49
 
50
- ## 快速开始
51
-
52
- ```bash
53
- npm install
54
- cp _env.local.example .env.local # 配置 DATABASE_URL
55
- npm run dev # 启动开发服务器
56
- ```
57
-
58
- ---
59
-
60
50
  ## 新增资源(以 posts 为例)
61
51
 
62
52
  ### 1. shared/ — 定义类型和校验
@@ -103,7 +93,7 @@ export const posts = pgTable("posts", {
103
93
  import { Router } from "express";
104
94
  import { db } from "../db/index";
105
95
  import { posts } from "../db/schema";
106
- import { createPostSchema } from "../../shared/api.interface";
96
+ import { createPostSchema } from "@shared/api.interface";
107
97
 
108
98
  const router = Router();
109
99
  router.get("/", async (_req, res) => {
@@ -128,20 +118,29 @@ app.use("/api/posts", postsRouter);
128
118
 
129
119
  ### 3. client/ — API 封装和页面
130
120
 
121
+ > ⚠️ **客户端所有 HTTP 请求必须使用 `axiosForBackend`**
122
+ >
123
+ > `axiosForBackend` 由 `@lark-apaas/client-toolkit-lite` 提供,内置平台鉴权和请求上下文。**禁止使用** `fetch`、`axios`、`XMLHttpRequest` 或其他 HTTP 客户端直接发起请求。
124
+
131
125
  `client/src/api/index.ts` 增加封装:
132
126
 
133
127
  ```typescript
134
- import type { CreatePostRequest, CreatePostResponse, ListPostsResponse } from "@shared/api.interface";
135
-
136
- export const postsApi = {
137
- list: () => request<ListPostsResponse>("/api/posts"),
138
- create: (data: CreatePostRequest) => request<CreatePostResponse>("/api/posts", {
139
- method: "POST", body: JSON.stringify(data),
140
- }),
141
- };
128
+ import type { ListPostsResponse } from "@shared/api.interface";
129
+
130
+ export async function listPosts(): Promise<ListPostsResponse> {
131
+ try {
132
+ const response = await axiosForBackend({
133
+ url: '/api/posts',
134
+ method: 'GET',
135
+ });
136
+ return response.data;
137
+ } catch (error) {
138
+ throw error;
139
+ }
140
+ }
142
141
  ```
143
142
 
144
- `client/src/pages/posts/index.tsx` 编写页面,`client/src/app.tsx` 注册路由。
143
+ `client/src/pages/PostsPage/PostsPage.tsx` 编写页面,`client/src/app.tsx` 注册路由。
145
144
 
146
145
  ---
147
146
 
@@ -150,7 +149,7 @@ export const postsApi = {
150
149
  **页面文件只做骨架编排,不包含具体 UI 实现。**
151
150
 
152
151
  ```tsx
153
- // client/src/pages/dashboard/index.tsx
152
+ // client/src/pages/DashboardPage/DashboardPage.tsx
154
153
  import { StatsSection } from "./components/stats-section";
155
154
  import { DataTableSection } from "./components/data-table-section";
156
155
 
@@ -169,29 +168,57 @@ export default function DashboardPage() {
169
168
  - 每个视觉上独立的区块拆为一个组件文件,即使只出现一次
170
169
  - 单个组件文件不超过 **150 行**,超出时进一步拆分子组件
171
170
  - 页面专属组件放在 `pages/<page>/components/`
172
- - 跨页面复用的组件放在 `client/src/components/`
173
- - 相同 UI 片段出现 **≥2 次**时,必须提取为可复用组件
171
+ - `client/src/components/` 仅存放基础 UI 组件(如 shadcn/ui),**禁止存放业务组件**
174
172
  - 文件名 kebab-case(`stat-card.tsx`),组件名 PascalCase(`StatCard`)
175
173
  - 组件之间**禁止循环引用**
176
174
 
175
+ ### Section 独立性(并行开发规范)
176
+
177
+ 每个 Section 级组件(如 `StatsSection`、`DataTableSection`)必须做到**完全自包含**:
178
+
179
+ **1. 页面文件 = 纯布局组合器**
180
+
181
+ - 页面入口文件(如 `DashboardPage.tsx`)只做 import + JSX 排列,**禁止包含** `useState`、`useEffect`、数据请求或业务逻辑
182
+ - 页面文件无 props 接口定义,不承担任何数据协调职责
183
+
184
+ **2. Section 自包含原则**
185
+
186
+ 每个 Section 组件独立拥有自己的:
187
+ - **数据获取**(API 调用、fetch)— 即使多个 Section 需要同一份数据,各自获取
188
+ - **状态管理**(useState、useReducer)
189
+ - **类型定义**(写在同文件或同目录下的 `types.ts`)
190
+ - **子组件**(如需拆分,平铺在 `components/` 目录下,禁止嵌套子目录)
191
+
192
+ **3. 禁止 Section 间横向依赖**
193
+
194
+ - 兄弟 Section 之间**禁止互相 import**
195
+ - 兄弟 Section 之间**禁止通过 Context、全局 store、事件总线、页面 props 共享状态**
196
+ - 每个 Section 可独立开发、独立测试,不依赖其他 Section 的存在
197
+
177
198
  ---
178
199
 
179
200
  ## 路由注册
180
201
 
181
- 新增页面在 `client/src/app.tsx` 中注册:
202
+ 新增页面在 `client/src/app.tsx` 中注册。
203
+
204
+ > ⚠️ **首页路由替换(必做)**
205
+ >
206
+ > 模板默认的 `HomePage` 是占位示例页,**不是业务首页**。开发时必须将 `index` 路由替换为真实的业务首页组件(如 `DashboardPage`),并删除 `HomePage` 目录。
207
+
208
+ **替换后的路由示例:**
182
209
 
183
210
  ```tsx
184
211
  <Route element={<Layout />}>
185
- <Route index element={<HomePage />} />
212
+ {/* index 路由指向真实的业务首页 */}
213
+ <Route index element={<DashboardPage />} />
186
214
  <Route path="todos" element={<TodosPage />} />
187
- <Route path="dashboard" element={<DashboardPage />} />
188
215
  <Route path="*" element={<NotFoundPage />} />
189
216
  </Route>
190
217
  ```
191
218
 
192
219
  **新增页面步骤:**
193
220
 
194
- 1. 在 `client/src/pages/` 下新建页面目录和 `index.tsx`
221
+ 1. 在 `client/src/pages/` 下新建页面目录(如 `SettingsPage`)和 `SettingsPage.tsx`
195
222
  2. 在 `app.tsx` 的 `<Routes>` 内添加 `<Route>` 配置
196
223
 
197
224
  **路由跳转必须使用 react-router-dom:**
@@ -281,10 +308,10 @@ export default function DashboardPage() {
281
308
 
282
309
  | 检查项 | 验收标准 |
283
310
  |--------|---------|
284
- | 页面拆分 | 页面文件只做骨架编排;每个区块为独立组件;单文件 ≤150 行 |
285
- | 组件复用 | 相同片段 ≥2 次已提取为组件;文件名 kebab-case,组件名 PascalCase |
286
- | 路由注册 | 新页面已在 `app.tsx` 注册;跳转使用 `<Link>` / `useNavigate()`,无 `<a href>` |
287
- | API 调用 | 统一在 `api/` 封装;使用 `@shared` 类型;组件内不直接 fetch |
311
+ | 页面拆分 | 页面文件只做骨架编排(无 state/effect/逻辑);每个 Section 自包含数据+状态+类型;兄弟 Section 间无互相 import;单文件 ≤150 行 |
312
+ | 命名规范 | 页面目录 PascalCase,页面入口文件与目录同名(PascalCase),组件文件名 kebab-case,组件名 PascalCase |
313
+ | 路由注册 | 默认 `HomePage` 已替换为业务首页;新页面已在 `app.tsx` 注册;跳转使用 `<Link>` / `useNavigate()`,无 `<a href>` |
314
+ | API 调用 | 统一在 `api/` 封装;必须使用 `axiosForBackend` 发起请求,禁止 `fetch`/`axios`;使用 `@shared` 类型 |
288
315
  | 输入校验 | zod schema 定义在 `shared/api.interface.ts`;server 和 client 共用 |
289
316
  | 主题色 | 使用语义化变量类(`bg-background`、`text-primary` 等);未硬编码颜色值 |
290
317
  | 主题修改 | 仅增量覆盖变更的变量;新增色同时注册 `:root` 和 `@theme inline` |
@@ -1,39 +1,17 @@
1
- import type {
2
- CreateTodoRequest,
3
- CreateTodoResponse,
4
- UpdateTodoRequest,
5
- UpdateTodoResponse,
6
- DeleteTodoResponse,
7
- ListTodosResponse,
8
- } from "@shared/api.interface";
1
+ import { axiosForBackend } from '@lark-apaas/client-toolkit-lite/utils/getAxiosForBackend';
2
+ // import type { ListPostsResponse } from '@shared/api.interface';
9
3
 
10
- async function request<T>(url: string, options?: RequestInit): Promise<T> {
11
- const res = await fetch(url, {
12
- headers: { "Content-Type": "application/json" },
13
- ...options,
14
- });
15
- if (!res.ok) {
16
- const body = await res.json().catch(() => ({}));
17
- throw new Error(body.error?.toString() ?? `Request failed: ${res.status}`);
18
- }
19
- return res.json();
20
- }
21
-
22
- export const todosApi = {
23
- list: () => request<ListTodosResponse>("/api/todos"),
24
-
25
- create: (data: CreateTodoRequest) =>
26
- request<CreateTodoResponse>("/api/todos", {
27
- method: "POST",
28
- body: JSON.stringify(data),
29
- }),
30
-
31
- update: (id: number, data: UpdateTodoRequest) =>
32
- request<UpdateTodoResponse>(`/api/todos/${id}`, {
33
- method: "PATCH",
34
- body: JSON.stringify(data),
35
- }),
36
-
37
- remove: (id: number) =>
38
- request<DeleteTodoResponse>(`/api/todos/${id}`, { method: "DELETE" }),
39
- };
4
+ // Add more API functions here, use axios instance (`axiosForBackend`) to make requests.
5
+ //
6
+ // 使用示例:
7
+ // export async function listPosts(): Promise<ListPostsResponse> {
8
+ // try {
9
+ // const response = await axiosForBackend({
10
+ // url: '/api/posts',
11
+ // method: 'GET',
12
+ // });
13
+ // return response.data;
14
+ // } catch (error) {
15
+ // throw error;
16
+ // }
17
+ // }
@@ -1,8 +1,7 @@
1
1
  import { BrowserRouter, Routes, Route } from "react-router-dom";
2
2
  import { Layout } from "@/components/layout";
3
- import HomePage from "@/pages/home";
4
- import TodosPage from "@/pages/todos";
5
- import NotFoundPage from "@/pages/not-found";
3
+ import HomePage from "@/pages/HomePage/HomePage";
4
+ import NotFoundPage from "@/pages/NotFoundPage/NotFoundPage";
6
5
 
7
6
  export default function App() {
8
7
  return (
@@ -10,7 +9,6 @@ export default function App() {
10
9
  <Routes>
11
10
  <Route element={<Layout />}>
12
11
  <Route index element={<HomePage />} />
13
- <Route path="todos" element={<TodosPage />} />
14
12
  <Route path="*" element={<NotFoundPage />} />
15
13
  </Route>
16
14
  </Routes>
@@ -2,10 +2,8 @@ import { Outlet } from "react-router-dom";
2
2
 
3
3
  export function Layout() {
4
4
  return (
5
- <div className="min-h-screen bg-background text-foreground">
6
- <main className="container mx-auto px-4 py-8">
7
- <Outlet />
8
- </main>
5
+ <div className="w-screen h-screen">
6
+ <Outlet />
9
7
  </div>
10
8
  );
11
9
  }
@@ -0,0 +1,12 @@
1
+ export default function HomePage() {
2
+ return (
3
+ <div className="flex flex-col items-center justify-center py-24">
4
+ <h1 className="text-5xl font-bold tracking-tight sm:text-7xl mb-4">
5
+ 👋 Hello OpenClaw
6
+ </h1>
7
+ <p className="text-lg text-muted-foreground">
8
+ Start building your app by editing <code className="px-1.5 py-0.5 rounded bg-muted font-mono text-sm">client/src/pages/HomePage/HomePage.tsx</code>
9
+ </p>
10
+ </div>
11
+ );
12
+ }
@@ -74,7 +74,7 @@
74
74
  "zod": "^4.3.6"
75
75
  },
76
76
  "devDependencies": {
77
- "@lark-apaas/coding-presets": "^0.1.0",
77
+ "@lark-apaas/coding-presets": "^0.2.0",
78
78
  "@lark-apaas/coding-vite-preset": "^0.1.0",
79
79
  "@types/express": "^5",
80
80
  "@types/node": "^24",
@@ -1,8 +1,11 @@
1
- import { pgTable, serial, text, boolean, timestamp } from "drizzle-orm/pg-core";
2
-
3
- export const todos = pgTable("todos", {
4
- id: serial("id").primaryKey(),
5
- title: text("title").notNull(),
6
- completed: boolean("completed").notNull().default(false),
7
- createdAt: timestamp("created_at").notNull().defaultNow(),
8
- });
1
+ // Drizzle schema 定义(可由工具生成)
2
+ //
3
+ // import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
4
+ //
5
+ // 使用示例:
6
+ // export const posts = pgTable("posts", {
7
+ // id: serial("id").primaryKey(),
8
+ // title: text("title").notNull(),
9
+ // content: text("content"),
10
+ // createdAt: timestamp("created_at").notNull().defaultNow(),
11
+ // });
@@ -1,6 +1,9 @@
1
1
  import type { Express } from "express";
2
- import todosRouter from "./todos";
2
+ // import postsRouter from "./posts";
3
3
 
4
4
  export function registerRoutes(app: Express) {
5
- app.use("/api/todos", todosRouter);
5
+ // 在此注册 API 路由
6
+ //
7
+ // 使用示例:
8
+ // app.use("/api/posts", postsRouter);
6
9
  }
@@ -1,23 +1,13 @@
1
1
  // API 接口契约定义
2
2
  // zod schema 做校验,z.infer 推导类型,前后端共享
3
-
4
- import { z } from "zod";
5
- import type { Todo } from "./types";
6
-
7
- // ----- Todos -----
8
-
9
- export const createTodoSchema = z.object({
10
- title: z.string().min(1).max(500),
11
- });
12
-
13
- export const updateTodoSchema = z.object({
14
- title: z.string().min(1).max(500).optional(),
15
- completed: z.boolean().optional(),
16
- });
17
-
18
- export type CreateTodoRequest = z.infer<typeof createTodoSchema>;
19
- export type UpdateTodoRequest = z.infer<typeof updateTodoSchema>;
20
- export type CreateTodoResponse = Todo;
21
- export type UpdateTodoResponse = Todo;
22
- export type DeleteTodoResponse = Todo;
23
- export type ListTodosResponse = Todo[];
3
+ //
4
+ // import { z } from "zod";
5
+ // import type { Post } from "./types";
6
+ //
7
+ // 使用示例:
8
+ // export const createPostSchema = z.object({
9
+ // title: z.string().min(1).max(200),
10
+ // content: z.string().optional(),
11
+ // });
12
+ // export type CreatePostRequest = z.infer<typeof createPostSchema>;
13
+ // export type CreatePostResponse = Post;
@@ -1,9 +1,10 @@
1
1
  // 前后端共享类型定义
2
2
  // shared 是最底层模块,不依赖 client 或 server
3
-
4
- export interface Todo {
5
- id: number;
6
- title: string;
7
- completed: boolean;
8
- createdAt: string;
9
- }
3
+ //
4
+ // 使用示例:
5
+ // export interface Post {
6
+ // id: number;
7
+ // title: string;
8
+ // content: string | null;
9
+ // createdAt: string;
10
+ // }
@@ -4,6 +4,8 @@
4
4
  "outDir": "./dist/server",
5
5
  "rootDir": ".",
6
6
  "noEmit": false,
7
+ "allowImportingTsExtensions": false,
8
+ "noUnusedParameters": false,
7
9
  "baseUrl": ".",
8
10
  "paths": {
9
11
  "@shared/*": ["./shared/*"]
@@ -1,20 +0,0 @@
1
- import { Link } from "react-router-dom";
2
-
3
- export default function HomePage() {
4
- return (
5
- <div className="flex flex-col items-center justify-center py-24">
6
- <h1 className="text-5xl font-bold tracking-tight sm:text-7xl mb-4">
7
- Hello OpenClaw
8
- </h1>
9
- <p className="text-lg text-muted-foreground mb-8">
10
- Apex full-stack template
11
- </p>
12
- <Link
13
- to="/todos"
14
- className="inline-flex items-center justify-center rounded-md bg-primary px-6 py-3 text-sm font-medium text-primary-foreground hover:bg-primary/90"
15
- >
16
- Try the Todos Demo
17
- </Link>
18
- </div>
19
- );
20
- }
@@ -1,40 +0,0 @@
1
- import { useState } from "react";
2
-
3
- interface TodoFormProps {
4
- onSubmit: (title: string) => Promise<void>;
5
- }
6
-
7
- export function TodoForm({ onSubmit }: TodoFormProps) {
8
- const [title, setTitle] = useState("");
9
- const [submitting, setSubmitting] = useState(false);
10
-
11
- const handleSubmit = async (e: React.FormEvent) => {
12
- e.preventDefault();
13
- const trimmed = title.trim();
14
- if (!trimmed) return;
15
- setSubmitting(true);
16
- await onSubmit(trimmed);
17
- setTitle("");
18
- setSubmitting(false);
19
- };
20
-
21
- return (
22
- <form onSubmit={handleSubmit} className="flex gap-2">
23
- <input
24
- type="text"
25
- value={title}
26
- onChange={(e) => setTitle(e.target.value)}
27
- placeholder="What needs to be done?"
28
- className="flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
29
- disabled={submitting}
30
- />
31
- <button
32
- type="submit"
33
- disabled={submitting || !title.trim()}
34
- className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50"
35
- >
36
- Add
37
- </button>
38
- </form>
39
- );
40
- }
@@ -1,43 +0,0 @@
1
- import type { Todo } from "@shared/types";
2
- import { Trash2 } from "lucide-react";
3
-
4
- interface TodoListProps {
5
- todos: Todo[];
6
- onToggle: (id: number, completed: boolean) => Promise<void>;
7
- onDelete: (id: number) => Promise<void>;
8
- }
9
-
10
- export function TodoList({ todos, onToggle, onDelete }: TodoListProps) {
11
- if (todos.length === 0) {
12
- return <p className="text-muted-foreground mt-8 text-center">No todos yet.</p>;
13
- }
14
-
15
- return (
16
- <ul className="mt-6 space-y-2">
17
- {todos.map((todo) => (
18
- <li
19
- key={todo.id}
20
- className="flex items-center gap-3 rounded-md border border-border px-4 py-3"
21
- >
22
- <input
23
- type="checkbox"
24
- checked={todo.completed}
25
- onChange={() => onToggle(todo.id, !todo.completed)}
26
- className="h-4 w-4 rounded border-border"
27
- />
28
- <span
29
- className={`flex-1 text-sm ${todo.completed ? "line-through text-muted-foreground" : ""}`}
30
- >
31
- {todo.title}
32
- </span>
33
- <button
34
- onClick={() => onDelete(todo.id)}
35
- className="text-muted-foreground hover:text-destructive transition-colors"
36
- >
37
- <Trash2 className="h-4 w-4" />
38
- </button>
39
- </li>
40
- ))}
41
- </ul>
42
- );
43
- }
@@ -1,47 +0,0 @@
1
- import { useEffect, useState } from "react";
2
- import type { Todo } from "@shared/types";
3
- import { todosApi } from "@/api";
4
- import { TodoForm } from "./components/todo-form";
5
- import { TodoList } from "./components/todo-list";
6
-
7
- export default function TodosPage() {
8
- const [todos, setTodos] = useState<Todo[]>([]);
9
- const [loading, setLoading] = useState(true);
10
-
11
- const refresh = async () => {
12
- const list = await todosApi.list();
13
- setTodos(list);
14
- setLoading(false);
15
- };
16
-
17
- useEffect(() => {
18
- refresh();
19
- }, []);
20
-
21
- const handleCreate = async (title: string) => {
22
- await todosApi.create({ title });
23
- await refresh();
24
- };
25
-
26
- const handleToggle = async (id: number, completed: boolean) => {
27
- await todosApi.update(id, { completed });
28
- await refresh();
29
- };
30
-
31
- const handleDelete = async (id: number) => {
32
- await todosApi.remove(id);
33
- await refresh();
34
- };
35
-
36
- return (
37
- <div className="max-w-2xl mx-auto py-12">
38
- <h1 className="text-3xl font-bold mb-8">Todos</h1>
39
- <TodoForm onSubmit={handleCreate} />
40
- {loading ? (
41
- <p className="text-muted-foreground mt-8">Loading...</p>
42
- ) : (
43
- <TodoList todos={todos} onToggle={handleToggle} onDelete={handleDelete} />
44
- )}
45
- </div>
46
- );
47
- }
@@ -1,53 +0,0 @@
1
- import { Router } from "express";
2
- import { eq, desc } from "drizzle-orm";
3
- import { db } from "../db/index";
4
- import { todos } from "../db/schema";
5
- import { createTodoSchema, updateTodoSchema } from "../../shared/api.interface";
6
-
7
- const router = Router();
8
-
9
- // GET /api/todos
10
- router.get("/", async (_req, res) => {
11
- const list = await db.select().from(todos).orderBy(desc(todos.createdAt));
12
- res.json(list);
13
- });
14
-
15
- // POST /api/todos
16
- router.post("/", async (req, res) => {
17
- const parsed = createTodoSchema.safeParse(req.body);
18
- if (!parsed.success) {
19
- res.status(400).json({ error: parsed.error.flatten() });
20
- return;
21
- }
22
- const [todo] = await db.insert(todos).values({ title: parsed.data.title }).returning();
23
- res.status(201).json(todo);
24
- });
25
-
26
- // PATCH /api/todos/:id
27
- router.patch("/:id", async (req, res) => {
28
- const id = Number(req.params.id);
29
- const parsed = updateTodoSchema.safeParse(req.body);
30
- if (!parsed.success) {
31
- res.status(400).json({ error: parsed.error.flatten() });
32
- return;
33
- }
34
- const [todo] = await db.update(todos).set(parsed.data).where(eq(todos.id, id)).returning();
35
- if (!todo) {
36
- res.status(404).json({ error: "Not found" });
37
- return;
38
- }
39
- res.json(todo);
40
- });
41
-
42
- // DELETE /api/todos/:id
43
- router.delete("/:id", async (req, res) => {
44
- const id = Number(req.params.id);
45
- const [todo] = await db.delete(todos).where(eq(todos.id, id)).returning();
46
- if (!todo) {
47
- res.status(404).json({ error: "Not found" });
48
- return;
49
- }
50
- res.json(todo);
51
- });
52
-
53
- export default router;