refactor: 代码审查修复 — 错误边界、DRY抽取、测试修复、合规性改进
- P1: server.ts 统一错误边界 (withErrorHandler + AppError),修复 3 个失败/卡死测试 - P2: db 层 wrap/paginateQuery 抽取,前端 handleResponse 抽取,parseIdFromUrl 抽取 - P3: middleware 验证消息中文化,Flex→Space 替换 - P0: docs/development/README.md 新增已知设计决策章节 - P3-11 setup 拆分已尝试回退(@testing-library/react preload 依赖无法拆分) - P3-13 config 层测试从本次变更移除
This commit is contained in:
@@ -1,10 +1,22 @@
|
||||
import type { SQL } from "drizzle-orm";
|
||||
import type { SQLiteTable } from "drizzle-orm/sqlite-core";
|
||||
|
||||
import Database from "bun:sqlite";
|
||||
import { and, sql } from "drizzle-orm";
|
||||
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||
import { join } from "node:path";
|
||||
|
||||
import type { Logger } from "../logger";
|
||||
|
||||
const DB_FILENAME = "alfred.db";
|
||||
|
||||
export interface PaginateResult<T> {
|
||||
items: T[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export function createDatabase(dataDir: string, logger: Logger): Database {
|
||||
const dbPath = join(dataDir, DB_FILENAME);
|
||||
const db = new Database(dbPath);
|
||||
@@ -17,3 +29,47 @@ export function createDatabase(dataDir: string, logger: Logger): Database {
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
export function paginateQuery<T extends SQLiteTable, R>(
|
||||
raw: Database,
|
||||
table: T,
|
||||
options: {
|
||||
conditions?: Array<SQL | undefined>;
|
||||
mapRow: (row: T["$inferSelect"]) => R;
|
||||
orderBy?: (table: T) => SQL | undefined;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
},
|
||||
): PaginateResult<R> {
|
||||
const db = wrap(raw);
|
||||
const where = options.conditions?.filter((c): c is SQL => c !== undefined);
|
||||
const whereClause = where && where.length > 0 ? and(...where) : undefined;
|
||||
|
||||
const countResult = db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(table)
|
||||
.where(whereClause)
|
||||
.get();
|
||||
|
||||
const total = Number(countResult?.count ?? 0);
|
||||
|
||||
const rows = db
|
||||
.select()
|
||||
.from(table)
|
||||
.where(whereClause)
|
||||
.orderBy(options.orderBy?.(table) ?? sql`1`)
|
||||
.limit(options.pageSize)
|
||||
.offset((options.page - 1) * options.pageSize)
|
||||
.all();
|
||||
|
||||
return {
|
||||
items: rows.map(options.mapRow),
|
||||
page: options.page,
|
||||
pageSize: options.pageSize,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
export function wrap(raw: Database) {
|
||||
return drizzle(raw);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import { and, desc, eq, like, or, sql } from "drizzle-orm";
|
||||
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||
import { desc, eq, like, or, sql } from "drizzle-orm";
|
||||
|
||||
import type { CreateModelRequest, Model, ModelCapability, UpdateModelRequest } from "../../shared/api";
|
||||
|
||||
import { paginateQuery, wrap } from "./connection";
|
||||
import { models, providers } from "./schema";
|
||||
|
||||
export function createModel(
|
||||
@@ -87,7 +87,6 @@ export function listModels(
|
||||
raw: Database,
|
||||
options: { keyword?: string; page: number; pageSize: number; providerId?: string },
|
||||
): { items: Model[]; page: number; pageSize: number; total: number } {
|
||||
const db = wrap(raw);
|
||||
const conditions = [];
|
||||
|
||||
if (options.providerId) {
|
||||
@@ -99,31 +98,13 @@ export function listModels(
|
||||
conditions.push(or(like(models.name, pattern), like(models.modelId, pattern))!);
|
||||
}
|
||||
|
||||
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
||||
|
||||
const countResult = db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(models)
|
||||
.where(where)
|
||||
.get();
|
||||
|
||||
const total = Number(countResult?.count ?? 0);
|
||||
|
||||
const rows = db
|
||||
.select()
|
||||
.from(models)
|
||||
.where(where)
|
||||
.orderBy(desc(models.createdAt))
|
||||
.limit(options.pageSize)
|
||||
.offset((options.page - 1) * options.pageSize)
|
||||
.all();
|
||||
|
||||
return {
|
||||
items: rows.map(toModel),
|
||||
return paginateQuery(raw, models, {
|
||||
conditions,
|
||||
mapRow: toModel,
|
||||
orderBy: () => desc(models.createdAt),
|
||||
page: options.page,
|
||||
pageSize: options.pageSize,
|
||||
total,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function updateModel(
|
||||
@@ -203,7 +184,3 @@ function toModel(row: typeof models.$inferSelect): Model {
|
||||
updatedAt: row.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
function wrap(raw: Database) {
|
||||
return drizzle(raw);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import { and, desc, eq, like, or, sql } from "drizzle-orm";
|
||||
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||
import { desc, eq, like, or } from "drizzle-orm";
|
||||
|
||||
import type { CreateProjectRequest, Project, ProjectStatus, UpdateProjectRequest } from "../../shared/api";
|
||||
|
||||
import { paginateQuery, wrap } from "./connection";
|
||||
import { projects } from "./schema";
|
||||
|
||||
export function archiveProject(raw: Database, id: string): { error: string; status: number } | { project: Project } {
|
||||
@@ -79,7 +79,6 @@ export function listProjects(
|
||||
raw: Database,
|
||||
options: { keyword?: string; page: number; pageSize: number; status?: ProjectStatus },
|
||||
): { items: Project[]; page: number; pageSize: number; total: number } {
|
||||
const db = wrap(raw);
|
||||
const conditions = [];
|
||||
|
||||
if (options.status) {
|
||||
@@ -91,31 +90,13 @@ export function listProjects(
|
||||
conditions.push(or(like(projects.name, pattern), like(projects.description, pattern))!);
|
||||
}
|
||||
|
||||
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
||||
|
||||
const countResult = db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(projects)
|
||||
.where(where)
|
||||
.get();
|
||||
|
||||
const total = Number(countResult?.count ?? 0);
|
||||
|
||||
const rows = db
|
||||
.select()
|
||||
.from(projects)
|
||||
.where(where)
|
||||
.orderBy(desc(projects.createdAt))
|
||||
.limit(options.pageSize)
|
||||
.offset((options.page - 1) * options.pageSize)
|
||||
.all();
|
||||
|
||||
return {
|
||||
items: rows.map(toProject),
|
||||
return paginateQuery(raw, projects, {
|
||||
conditions,
|
||||
mapRow: toProject,
|
||||
orderBy: () => desc(projects.createdAt),
|
||||
page: options.page,
|
||||
pageSize: options.pageSize,
|
||||
total,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function restoreProject(raw: Database, id: string): { error: string; status: number } | { project: Project } {
|
||||
@@ -187,7 +168,3 @@ function toProject(row: typeof projects.$inferSelect): Project {
|
||||
updatedAt: row.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
function wrap(raw: Database) {
|
||||
return drizzle(raw);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import { and, desc, eq, like, sql } from "drizzle-orm";
|
||||
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||
import { desc, eq, like } from "drizzle-orm";
|
||||
|
||||
import type { CreateProviderRequest, Provider, ProviderOption, UpdateProviderRequest } from "../../shared/api";
|
||||
|
||||
import { paginateQuery, wrap } from "./connection";
|
||||
import { providers } from "./schema";
|
||||
|
||||
export function createProvider(
|
||||
@@ -80,7 +80,6 @@ export function listProviders(
|
||||
raw: Database,
|
||||
options: { keyword?: string; page: number; pageSize: number },
|
||||
): { items: Provider[]; page: number; pageSize: number; total: number } {
|
||||
const db = wrap(raw);
|
||||
const conditions = [];
|
||||
|
||||
if (options.keyword) {
|
||||
@@ -88,31 +87,13 @@ export function listProviders(
|
||||
conditions.push(like(providers.name, pattern));
|
||||
}
|
||||
|
||||
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
||||
|
||||
const countResult = db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(providers)
|
||||
.where(where)
|
||||
.get();
|
||||
|
||||
const total = Number(countResult?.count ?? 0);
|
||||
|
||||
const rows = db
|
||||
.select()
|
||||
.from(providers)
|
||||
.where(where)
|
||||
.orderBy(desc(providers.createdAt))
|
||||
.limit(options.pageSize)
|
||||
.offset((options.page - 1) * options.pageSize)
|
||||
.all();
|
||||
|
||||
return {
|
||||
items: rows.map(toProvider),
|
||||
return paginateQuery(raw, providers, {
|
||||
conditions,
|
||||
mapRow: toProvider,
|
||||
orderBy: () => desc(providers.createdAt),
|
||||
page: options.page,
|
||||
pageSize: options.pageSize,
|
||||
total,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function updateProvider(
|
||||
@@ -179,7 +160,3 @@ function toProvider(row: typeof providers.$inferSelect): Provider {
|
||||
updatedAt: row.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
function wrap(raw: Database) {
|
||||
return drizzle(raw);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user