UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

218 lines (217 loc) 7.43 kB
/** * GuardViolationRepository — Guard 违反记录的仓储实现 * * 从 ViolationsStore 提取的数据操作, * 使用 Drizzle 类型安全 API 操作 guard_violations 表。 */ import { asc, count, desc, eq, sql } from 'drizzle-orm'; import { guardViolations } from '../../infrastructure/database/drizzle/schema.js'; import { RepositoryBase } from '../base/RepositoryBase.js'; /* ═══ Repository 实现 ═══ */ export class GuardViolationRepositoryImpl extends RepositoryBase { /** 最大保留条数 */ static MAX_RUNS = 200; constructor(drizzle) { super(drizzle, guardViolations); } /* ─── CRUD ─── */ async findById(id) { const row = this.drizzle.select().from(this.table).where(eq(this.table.id, id)).limit(1).get(); return row ? this.#mapRow(row) : null; } async create(data) { this.drizzle .insert(this.table) .values({ id: data.id, filePath: data.filePath, triggeredAt: data.triggeredAt, violationCount: data.violationCount, summary: data.summary ?? '', violationsJson: JSON.stringify(data.violations), createdAt: data.createdAt, }) .run(); return (await this.findById(data.id)); } async delete(id) { const result = this.drizzle.delete(this.table).where(eq(this.table.id, id)).run(); return result.changes > 0; } /* ─── 去重查询 ─── */ /** 获取指定文件的最近一条记录 (用于去重比较) */ async getLastByFile(filePath) { const row = this.drizzle .select({ id: this.table.id, violationsJson: this.table.violationsJson, }) .from(this.table) .where(eq(this.table.filePath, filePath)) .orderBy(desc(this.table.createdAt)) .limit(1) .get(); return row ? { id: row.id, violationsJson: row.violationsJson ?? '[]' } : null; } /** 刷新已有记录的时间戳 (去重命中时) */ async refreshTimestamp(id) { this.drizzle .update(this.table) .set({ triggeredAt: new Date().toISOString(), createdAt: Math.floor(Date.now() / 1000), }) .where(eq(this.table.id, id)) .run(); } /* ─── 查询 ─── */ /** 获取所有运行记录 (最旧在前) */ async getRuns() { const rows = this.drizzle.select().from(this.table).orderBy(asc(this.table.createdAt)).all(); return rows.map((r) => this.#mapRow(r)); } /** 按文件路径查询 */ async getRunsByFile(filePath) { const rows = this.drizzle .select() .from(this.table) .where(eq(this.table.filePath, filePath)) .orderBy(asc(this.table.createdAt)) .all(); return rows.map((r) => this.#mapRow(r)); } /** 获取最近 N 条记录 */ async getRecentRuns(n = 20) { const rows = this.drizzle .select() .from(this.table) .orderBy(desc(this.table.createdAt), sql `rowid DESC`) .limit(n) .all(); return rows.reverse().map((r) => this.#mapRow(r)); } /** 分页查询 */ async list(filters = {}, options = {}) { const { page = 1, limit = 20 } = options; const offset = (page - 1) * limit; const condition = filters.file ? eq(this.table.filePath, filters.file) : undefined; const rows = this.drizzle .select() .from(this.table) .where(condition) .orderBy(desc(this.table.createdAt)) .limit(limit) .offset(offset) .all(); const [totalRow] = this.drizzle .select({ cnt: count() }) .from(this.table) .where(condition) .all(); const total = totalRow?.cnt ?? 0; return { data: rows.map((r) => this.#mapRow(r)), pagination: { page, limit, total, pages: Math.ceil(total / limit) }, }; } /* ─── 统计 ─── */ /** 获取统计汇总 */ async getStats() { const [row] = this.drizzle .select({ totalRuns: count(), totalViolations: sql `COALESCE(SUM(${this.table.violationCount}), 0)`, lastRunAt: sql `MAX(${this.table.triggeredAt})`, }) .from(this.table) .all(); const totalRuns = row?.totalRuns ?? 0; const totalViolations = row?.totalViolations ?? 0; return { totalRuns, totalViolations, averageViolationsPerRun: totalRuns > 0 ? (totalViolations / totalRuns).toFixed(2) : 0, lastRunAt: row?.lastRunAt ?? null, }; } /** * 按规则 ID 聚合统计 * 利用 SQLite json_each 展开 violations_json 数组 * * json_each 是 SQLite 专有函数,Drizzle 无 typed API (ORM limitation) */ async getStatsByRule() { try { const rows = this.drizzle.all(sql ` SELECT json_extract(j.value, '$.ruleId') AS ruleId, json_extract(j.value, '$.severity') AS severity, COUNT(*) AS count FROM ${this.table} gv, json_each(gv.violations_json) j WHERE json_extract(j.value, '$.ruleId') IS NOT NULL GROUP BY ruleId, severity ORDER BY count DESC `); return rows; } catch { return []; } } /* ─── 容量控制 ─── */ /** 截断超限记录,保留最新 maxRuns 条 */ async enforceCapacity(maxRuns = GuardViolationRepositoryImpl.MAX_RUNS) { const result = this.drizzle .delete(this.table) .where(sql `${this.table.id} NOT IN ( SELECT ${this.table.id} FROM ${this.table} ORDER BY ${this.table.createdAt} DESC LIMIT ${maxRuns} )`) .run(); return result.changes; } /** 清空所有记录 */ async clearAll() { this.drizzle.delete(this.table).run(); } /** 清除指定文件的记录 */ async clearByFile(filePath) { const result = this.drizzle.delete(this.table).where(eq(this.table.filePath, filePath)).run(); return result.changes; } /** * 最近的 violation JSON 列表 (CoverageAnalyzer.#getRecentViolations) */ findRecentViolationsJson(limit) { return this.drizzle .select({ filePath: this.table.filePath, violationsJson: this.table.violationsJson, }) .from(this.table) .orderBy(desc(this.table.createdAt)) .limit(limit) .all(); } /* ─── 内部辅助 ─── */ #mapRow(row) { return { id: row.id, filePath: row.filePath, triggeredAt: row.triggeredAt, violationCount: row.violationCount ?? 0, summary: row.summary ?? '', violations: safeParseJSON(row.violationsJson, []), createdAt: row.createdAt, }; } } function safeParseJSON(str, fallback) { try { return str ? JSON.parse(str) : fallback; } catch { return fallback; } }