原来只有 `apps/web` 在 Prettier 下(配置在 `apps/web/.prettierrc.toml`、脚本在 web 的 package.json),后端和契约从来没格式化过 —— 手写在 100 列上下,`db/schema.ts` 还是 drizzle-kit pull 留下的 tab 缩进。两套口径分叉久了,跨端改一处就得记着「这边 什么风格」。 - 配置搬到根目录 `.prettierrc.toml`,内容不变(`semi=false`,其余全默认, printWidth 80 —— 和前端已有的格式一致,不另立一套宽度); - 脚本统一成根目录 `bun run fmt`,覆盖 `apps/*/src`、`apps/web/tests` 和两个构建 配置;web 自己那份 `fmt` 和重复的 prettier 依赖删掉; - `.prettierignore` 挡掉两类不该碰的:drizzle-kit 生成的 `src/db/meta/` 结构快照 (它是 db:generate 的比对输入,只该由 drizzle-kit 写)、unplugin 每次 dev 都会 重写的 `auto-imports.d.ts` / `components.d.ts`; - 全量跑了一遍。纯格式,无行为改动:api typecheck / check:routes / check:ast、 前端 type-check 全过,起 api 打了接口确认正常。前端这 39 个文件的小改动是 prettier 版本漂移(类型断言的换行口径变了),不是新配置带来的。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -40,7 +40,11 @@ function serializeTutorial(row: {
|
||||
|
||||
function selectTutorial(id: number) {
|
||||
return db
|
||||
.select({ tutorial: schema.tutorial, user: schema.user, realName: schema.userProfile.realName })
|
||||
.select({
|
||||
tutorial: schema.tutorial,
|
||||
user: schema.user,
|
||||
realName: schema.userProfile.realName,
|
||||
})
|
||||
.from(schema.tutorial)
|
||||
.innerJoin(schema.user, eq(schema.tutorial.createdById, schema.user.id))
|
||||
.leftJoin(schema.userProfile, eq(schema.userProfile.userId, schema.user.id))
|
||||
@@ -50,7 +54,11 @@ function selectTutorial(id: number) {
|
||||
|
||||
adminTutorialRoutes.get("/tutorials", requireSuperAdmin, async (c) => {
|
||||
const rows = await db
|
||||
.select({ tutorial: schema.tutorial, user: schema.user, realName: schema.userProfile.realName })
|
||||
.select({
|
||||
tutorial: schema.tutorial,
|
||||
user: schema.user,
|
||||
realName: schema.userProfile.realName,
|
||||
})
|
||||
.from(schema.tutorial)
|
||||
.innerJoin(schema.user, eq(schema.tutorial.createdById, schema.user.id))
|
||||
.leftJoin(schema.userProfile, eq(schema.userProfile.userId, schema.user.id))
|
||||
@@ -64,62 +72,98 @@ adminTutorialRoutes.get("/tutorials", requireSuperAdmin, async (c) => {
|
||||
})
|
||||
|
||||
adminTutorialRoutes.post("/tutorials", requireSuperAdmin, async (c) => {
|
||||
const parsed = createTutorialRequestSchema.safeParse(await c.req.json().catch(() => null))
|
||||
const parsed = createTutorialRequestSchema.safeParse(
|
||||
await c.req.json().catch(() => null),
|
||||
)
|
||||
if (!parsed.success) {
|
||||
return failure(c, 400, "invalid-request", parsed.error.issues[0]?.message ?? "Invalid payload")
|
||||
return failure(
|
||||
c,
|
||||
400,
|
||||
"invalid-request",
|
||||
parsed.error.issues[0]?.message ?? "Invalid payload",
|
||||
)
|
||||
}
|
||||
const now = new Date().toISOString()
|
||||
const [created] = await db.insert(schema.tutorial).values({
|
||||
...parsed.data,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
createdById: c.get("user")!.id,
|
||||
}).returning({ id: schema.tutorial.id })
|
||||
const [created] = await db
|
||||
.insert(schema.tutorial)
|
||||
.values({
|
||||
...parsed.data,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
createdById: c.get("user")!.id,
|
||||
})
|
||||
.returning({ id: schema.tutorial.id })
|
||||
const [row] = await selectTutorial(created!.id)
|
||||
return success(c, serializeTutorial(row!), 201)
|
||||
})
|
||||
|
||||
adminTutorialRoutes.get("/tutorials/:id", requireSuperAdmin, async (c) => {
|
||||
const [row] = await selectTutorial(queryInteger(c.req.param("id"), 0, { min: 1 }))
|
||||
if (!row) return failure(c, 404, "tutorial-not-found", "Tutorial does not exist")
|
||||
const [row] = await selectTutorial(
|
||||
queryInteger(c.req.param("id"), 0, { min: 1 }),
|
||||
)
|
||||
if (!row)
|
||||
return failure(c, 404, "tutorial-not-found", "Tutorial does not exist")
|
||||
return success(c, serializeTutorial(row))
|
||||
})
|
||||
|
||||
adminTutorialRoutes.put("/tutorials/:id", requireSuperAdmin, async (c) => {
|
||||
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
||||
const parsed = updateTutorialRequestSchema.safeParse(await c.req.json().catch(() => null))
|
||||
const parsed = updateTutorialRequestSchema.safeParse(
|
||||
await c.req.json().catch(() => null),
|
||||
)
|
||||
if (!parsed.success) {
|
||||
return failure(c, 400, "invalid-request", parsed.error.issues[0]?.message ?? "Invalid payload")
|
||||
return failure(
|
||||
c,
|
||||
400,
|
||||
"invalid-request",
|
||||
parsed.error.issues[0]?.message ?? "Invalid payload",
|
||||
)
|
||||
}
|
||||
const updated = await db.update(schema.tutorial)
|
||||
const updated = await db
|
||||
.update(schema.tutorial)
|
||||
.set({ ...parsed.data, updatedAt: new Date().toISOString() })
|
||||
.where(eq(schema.tutorial.id, id)).returning({ id: schema.tutorial.id })
|
||||
if (updated.length === 0) return failure(c, 404, "tutorial-not-found", "Tutorial does not exist")
|
||||
.where(eq(schema.tutorial.id, id))
|
||||
.returning({ id: schema.tutorial.id })
|
||||
if (updated.length === 0)
|
||||
return failure(c, 404, "tutorial-not-found", "Tutorial does not exist")
|
||||
const [row] = await selectTutorial(id)
|
||||
return success(c, serializeTutorial(row!))
|
||||
})
|
||||
|
||||
adminTutorialRoutes.put("/tutorials/:id/visibility", requireSuperAdmin, async (c) => {
|
||||
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
||||
const parsed = setTutorialVisibilityRequestSchema.safeParse(await c.req.json().catch(() => null))
|
||||
if (!parsed.success) return failure(c, 400, "invalid-request", "isPublic is required")
|
||||
// 只改可见性,不动 updatedAt —— 上下架不是内容修改,改了会打乱按更新时间排序的直觉
|
||||
const updated = await db.update(schema.tutorial)
|
||||
.set({ isPublic: parsed.data.isPublic })
|
||||
.where(eq(schema.tutorial.id, id)).returning({ id: schema.tutorial.id })
|
||||
if (updated.length === 0) return failure(c, 404, "tutorial-not-found", "Tutorial does not exist")
|
||||
const [row] = await selectTutorial(id)
|
||||
return success(c, serializeTutorial(row!))
|
||||
})
|
||||
adminTutorialRoutes.put(
|
||||
"/tutorials/:id/visibility",
|
||||
requireSuperAdmin,
|
||||
async (c) => {
|
||||
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
||||
const parsed = setTutorialVisibilityRequestSchema.safeParse(
|
||||
await c.req.json().catch(() => null),
|
||||
)
|
||||
if (!parsed.success)
|
||||
return failure(c, 400, "invalid-request", "isPublic is required")
|
||||
// 只改可见性,不动 updatedAt —— 上下架不是内容修改,改了会打乱按更新时间排序的直觉
|
||||
const updated = await db
|
||||
.update(schema.tutorial)
|
||||
.set({ isPublic: parsed.data.isPublic })
|
||||
.where(eq(schema.tutorial.id, id))
|
||||
.returning({ id: schema.tutorial.id })
|
||||
if (updated.length === 0)
|
||||
return failure(c, 404, "tutorial-not-found", "Tutorial does not exist")
|
||||
const [row] = await selectTutorial(id)
|
||||
return success(c, serializeTutorial(row!))
|
||||
},
|
||||
)
|
||||
|
||||
adminTutorialRoutes.delete("/tutorials/:id", requireSuperAdmin, async (c) => {
|
||||
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
||||
// 练习与学习留痕都随教程一起没:exercise.tutorial_id 与 tutorial_progress.tutorial_id
|
||||
// 都是库级 CASCADE。**加子表时要回来想一遍该 CASCADE 还是该拦住**,
|
||||
// 别默认新表会自己连坐 —— 0010 只改了当时存在的那批外键。
|
||||
const deleted = await db.delete(schema.tutorial).where(eq(schema.tutorial.id, id))
|
||||
const deleted = await db
|
||||
.delete(schema.tutorial)
|
||||
.where(eq(schema.tutorial.id, id))
|
||||
.returning({ id: schema.tutorial.id })
|
||||
if (deleted.length === 0) return failure(c, 404, "tutorial-not-found", "Tutorial does not exist")
|
||||
if (deleted.length === 0)
|
||||
return failure(c, 404, "tutorial-not-found", "Tutorial does not exist")
|
||||
return success(c, null)
|
||||
})
|
||||
|
||||
@@ -136,52 +180,96 @@ function serializeExercise(row: typeof schema.exercise.$inferSelect) {
|
||||
|
||||
// 练习挂在教程下,路径嵌套 —— 旧后端是 ?tutorial_id= 查询参数,
|
||||
// 但它本来就是一对多的从属关系,嵌套路径更贴事实,也省掉「忘了传 tutorial_id」这类错误
|
||||
adminTutorialRoutes.get("/tutorials/:id/exercises", requireSuperAdmin, async (c) => {
|
||||
const rows = await db.select().from(schema.exercise)
|
||||
.where(eq(schema.exercise.tutorialId, queryInteger(c.req.param("id"), 0, { min: 1 })))
|
||||
.orderBy(asc(schema.exercise.order), asc(schema.exercise.id))
|
||||
return success(c, rows.map(serializeExercise))
|
||||
})
|
||||
adminTutorialRoutes.get(
|
||||
"/tutorials/:id/exercises",
|
||||
requireSuperAdmin,
|
||||
async (c) => {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(schema.exercise)
|
||||
.where(
|
||||
eq(
|
||||
schema.exercise.tutorialId,
|
||||
queryInteger(c.req.param("id"), 0, { min: 1 }),
|
||||
),
|
||||
)
|
||||
.orderBy(asc(schema.exercise.order), asc(schema.exercise.id))
|
||||
return success(c, rows.map(serializeExercise))
|
||||
},
|
||||
)
|
||||
|
||||
adminTutorialRoutes.post("/exercises", requireSuperAdmin, async (c) => {
|
||||
const parsed = createExerciseRequestSchema.safeParse(await c.req.json().catch(() => null))
|
||||
const parsed = createExerciseRequestSchema.safeParse(
|
||||
await c.req.json().catch(() => null),
|
||||
)
|
||||
if (!parsed.success) {
|
||||
return failure(c, 400, "invalid-request", parsed.error.issues[0]?.message ?? "Invalid payload")
|
||||
return failure(
|
||||
c,
|
||||
400,
|
||||
"invalid-request",
|
||||
parsed.error.issues[0]?.message ?? "Invalid payload",
|
||||
)
|
||||
}
|
||||
const [tutorial] = await db.select({ id: schema.tutorial.id }).from(schema.tutorial)
|
||||
.where(eq(schema.tutorial.id, parsed.data.tutorialId)).limit(1)
|
||||
if (!tutorial) return failure(c, 404, "tutorial-not-found", "Tutorial does not exist")
|
||||
const [tutorial] = await db
|
||||
.select({ id: schema.tutorial.id })
|
||||
.from(schema.tutorial)
|
||||
.where(eq(schema.tutorial.id, parsed.data.tutorialId))
|
||||
.limit(1)
|
||||
if (!tutorial)
|
||||
return failure(c, 404, "tutorial-not-found", "Tutorial does not exist")
|
||||
const dataError = exerciseDataError(parsed.data.type, parsed.data.data)
|
||||
if (dataError) return failure(c, 400, "invalid-exercise", dataError)
|
||||
const [created] = await db.insert(schema.exercise).values({
|
||||
tutorialId: parsed.data.tutorialId,
|
||||
type: parsed.data.type,
|
||||
data: parsed.data.data,
|
||||
order: parsed.data.order,
|
||||
createdAt: new Date().toISOString(),
|
||||
}).returning()
|
||||
const [created] = await db
|
||||
.insert(schema.exercise)
|
||||
.values({
|
||||
tutorialId: parsed.data.tutorialId,
|
||||
type: parsed.data.type,
|
||||
data: parsed.data.data,
|
||||
order: parsed.data.order,
|
||||
createdAt: new Date().toISOString(),
|
||||
})
|
||||
.returning()
|
||||
return success(c, serializeExercise(created!), 201)
|
||||
})
|
||||
|
||||
adminTutorialRoutes.put("/exercises/:id", requireSuperAdmin, async (c) => {
|
||||
const parsed = updateExerciseRequestSchema.safeParse(await c.req.json().catch(() => null))
|
||||
const parsed = updateExerciseRequestSchema.safeParse(
|
||||
await c.req.json().catch(() => null),
|
||||
)
|
||||
if (!parsed.success) {
|
||||
return failure(c, 400, "invalid-request", parsed.error.issues[0]?.message ?? "Invalid payload")
|
||||
return failure(
|
||||
c,
|
||||
400,
|
||||
"invalid-request",
|
||||
parsed.error.issues[0]?.message ?? "Invalid payload",
|
||||
)
|
||||
}
|
||||
const dataError = exerciseDataError(parsed.data.type, parsed.data.data)
|
||||
if (dataError) return failure(c, 400, "invalid-exercise", dataError)
|
||||
const [updated] = await db.update(schema.exercise)
|
||||
.set({ type: parsed.data.type, data: parsed.data.data, order: parsed.data.order })
|
||||
.where(eq(schema.exercise.id, queryInteger(c.req.param("id"), 0, { min: 1 })))
|
||||
const [updated] = await db
|
||||
.update(schema.exercise)
|
||||
.set({
|
||||
type: parsed.data.type,
|
||||
data: parsed.data.data,
|
||||
order: parsed.data.order,
|
||||
})
|
||||
.where(
|
||||
eq(schema.exercise.id, queryInteger(c.req.param("id"), 0, { min: 1 })),
|
||||
)
|
||||
.returning()
|
||||
if (!updated) return failure(c, 404, "exercise-not-found", "Exercise does not exist")
|
||||
if (!updated)
|
||||
return failure(c, 404, "exercise-not-found", "Exercise does not exist")
|
||||
return success(c, serializeExercise(updated))
|
||||
})
|
||||
|
||||
adminTutorialRoutes.delete("/exercises/:id", requireSuperAdmin, async (c) => {
|
||||
const deleted = await db.delete(schema.exercise)
|
||||
.where(eq(schema.exercise.id, queryInteger(c.req.param("id"), 0, { min: 1 })))
|
||||
const deleted = await db
|
||||
.delete(schema.exercise)
|
||||
.where(
|
||||
eq(schema.exercise.id, queryInteger(c.req.param("id"), 0, { min: 1 })),
|
||||
)
|
||||
.returning({ id: schema.exercise.id })
|
||||
if (deleted.length === 0) return failure(c, 404, "exercise-not-found", "Exercise does not exist")
|
||||
if (deleted.length === 0)
|
||||
return failure(c, 404, "exercise-not-found", "Exercise does not exist")
|
||||
return success(c, null)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user