feat(query): unified filter opts across all list-returning APIs
Add sessions() as first-class entry point for session discovery with project/time/branch filters. Extend summaries, workflows, failures, subagents, fileHistory with the same opts vocabulary that search() already had — no more pulling full datasets to filter client-side.
This commit is contained in:
+75
-20
@@ -2,6 +2,28 @@ import { openDb, readLines, fs, path } from './db.mjs';
|
||||
|
||||
const ERROR_PATS = ['error','Error','ENOENT','failed','Failed','FAILED','permission denied','Permission denied','EPERM','EACCES','command not found','No such file','Exit code'];
|
||||
|
||||
function normalizeOpts(optsOrScalar, scalarKey = 'sessionId') {
|
||||
if (optsOrScalar == null) return {};
|
||||
if (typeof optsOrScalar === 'string') return { [scalarKey]: optsOrScalar };
|
||||
if (typeof optsOrScalar === 'number') return { limit: optsOrScalar };
|
||||
return optsOrScalar;
|
||||
}
|
||||
|
||||
function buildWhere(opts, aliases) {
|
||||
const clauses = [];
|
||||
const params = [];
|
||||
if (opts.sessionId) { clauses.push(`${aliases.sessionId} = ?`); params.push(opts.sessionId); }
|
||||
if (opts.sessions?.length) {
|
||||
clauses.push(`${aliases.sessionId} IN (${opts.sessions.map(() => '?').join(',')})`);
|
||||
params.push(...opts.sessions);
|
||||
}
|
||||
if (opts.project) { clauses.push(`${aliases.project} LIKE ?`); params.push(opts.project); }
|
||||
if (opts.after) { clauses.push(`${aliases.timestamp} > ?`); params.push(opts.after); }
|
||||
if (opts.before) { clauses.push(`${aliases.timestamp} < ?`); params.push(opts.before); }
|
||||
if (opts.branch) { clauses.push(`${aliases.branch} = ?`); params.push(opts.branch); }
|
||||
return { where: clauses.length ? clauses.join(' AND ') : '1=1', params };
|
||||
}
|
||||
|
||||
function createQueryApi(db) {
|
||||
const q = (sql, ...p) => db.prepare(sql).all(...p);
|
||||
|
||||
@@ -56,16 +78,28 @@ function createQueryApi(db) {
|
||||
|
||||
const thread = (sid) => db.prepare('SELECT * FROM messages WHERE session_id=? ORDER BY timestamp').all(sid);
|
||||
|
||||
const subagents = (sid) => {
|
||||
return db.prepare('SELECT * FROM subagents WHERE session_id=?').all(sid).map(r => {
|
||||
const subagents = (optsOrSid) => {
|
||||
const opts = normalizeOpts(optsOrSid);
|
||||
const { limit = 100 } = opts;
|
||||
const needsJoin = opts.project || opts.branch;
|
||||
const { where, params } = buildWhere(opts, { sessionId: 'sa.session_id', project: 's.project', timestamp: 'sa.session_id', branch: 's.git_branch' });
|
||||
params.push(limit);
|
||||
const join = needsJoin ? 'LEFT JOIN sessions s ON s.id=sa.session_id' : '';
|
||||
return db.prepare(`SELECT sa.* FROM subagents sa ${join} WHERE ${where} LIMIT ?`).all(...params).map(r => {
|
||||
const c = db.prepare('SELECT COUNT(*) as c FROM messages WHERE agent_id=?').get(r.agent_id);
|
||||
return { ...r, messageCount: c?.c || 0 };
|
||||
});
|
||||
};
|
||||
|
||||
const workflows = (sid) => sid
|
||||
? db.prepare('SELECT * FROM workflows WHERE session_id=? ORDER BY timestamp').all(sid)
|
||||
: db.prepare('SELECT * FROM workflows ORDER BY timestamp DESC').all();
|
||||
const workflows = (optsOrSid) => {
|
||||
const opts = normalizeOpts(optsOrSid);
|
||||
const { limit = 100 } = opts;
|
||||
const needsJoin = opts.project || opts.branch;
|
||||
const { where, params } = buildWhere(opts, { sessionId: 'w.session_id', project: 's.project', timestamp: 'w.timestamp', branch: 's.git_branch' });
|
||||
params.push(limit);
|
||||
const join = needsJoin ? 'LEFT JOIN sessions s ON s.id=w.session_id' : '';
|
||||
return db.prepare(`SELECT w.* FROM workflows w ${join} WHERE ${where} ORDER BY w.timestamp DESC LIMIT ?`).all(...params);
|
||||
};
|
||||
|
||||
const workflowTree = (runId) => {
|
||||
const wf = db.prepare('SELECT * FROM workflows WHERE run_id=?').get(runId);
|
||||
@@ -76,23 +110,32 @@ function createQueryApi(db) {
|
||||
return { ...wf, agents };
|
||||
};
|
||||
|
||||
const fileHistory = (fp) => {
|
||||
const fileHistory = (fp, opts = {}) => {
|
||||
const { limit = 200, after, before } = opts;
|
||||
let where = 'tc.file_path=?';
|
||||
const params = [fp];
|
||||
if (after) { where += ' AND m.timestamp > ?'; params.push(after); }
|
||||
if (before) { where += ' AND m.timestamp < ?'; params.push(before); }
|
||||
params.push(limit);
|
||||
return db.prepare(
|
||||
'SELECT tc.*,s.title as s_title,s.project as s_project FROM tool_calls tc LEFT JOIN sessions s ON s.id=tc.session_id WHERE tc.file_path=? ORDER BY tc.id'
|
||||
).all(fp).map(r => ({
|
||||
`SELECT tc.*,s.title as s_title,s.project as s_project,m.timestamp as ts FROM tool_calls tc LEFT JOIN sessions s ON s.id=tc.session_id LEFT JOIN messages m ON m.uuid=tc.message_uuid WHERE ${where} ORDER BY m.timestamp LIMIT ?`
|
||||
).all(...params).map(r => ({
|
||||
toolCall: { id: r.id, message_uuid: r.message_uuid, name: r.name, input_json: r.input_json },
|
||||
session: { id: r.session_id, title: r.s_title, project: r.s_project },
|
||||
timestamp: db.prepare('SELECT timestamp FROM messages WHERE uuid=?').get(r.message_uuid)?.timestamp,
|
||||
timestamp: r.ts,
|
||||
}));
|
||||
};
|
||||
|
||||
const failures = (sid) => {
|
||||
const likeClauses = ERROR_PATS.map(() => 'content LIKE ?').join(' OR ');
|
||||
const failures = (optsOrSid) => {
|
||||
const opts = normalizeOpts(optsOrSid);
|
||||
const { limit = 50 } = opts;
|
||||
const likeClauses = ERROR_PATS.map(() => 'tr.content LIKE ?').join(' OR ');
|
||||
const likeParams = ERROR_PATS.map(p => `%${p}%`);
|
||||
let query = `SELECT * FROM tool_results WHERE (${likeClauses})`;
|
||||
const params = [...likeParams];
|
||||
if (sid) { query += ' AND session_id=?'; params.push(sid); }
|
||||
const rows = db.prepare(query).all(...params);
|
||||
const needsJoin = opts.project || opts.branch;
|
||||
const { where, params: filterParams } = buildWhere(opts, { sessionId: 'tr.session_id', project: 's.project', timestamp: 'rm.timestamp', branch: 's.git_branch' });
|
||||
const join = needsJoin ? 'LEFT JOIN sessions s ON s.id=tr.session_id' : '';
|
||||
const allParams = [...likeParams, ...filterParams, limit];
|
||||
const rows = db.prepare(`SELECT tr.* FROM tool_results tr ${join} LEFT JOIN messages rm ON rm.uuid=tr.message_uuid WHERE (${likeClauses}) AND ${where} LIMIT ?`).all(...allParams);
|
||||
return rows.map(r => {
|
||||
const tc = db.prepare('SELECT * FROM tool_calls WHERE id=?').get(r.tool_use_id);
|
||||
const session = db.prepare('SELECT * FROM sessions WHERE id=?').get(r.session_id);
|
||||
@@ -102,11 +145,23 @@ function createQueryApi(db) {
|
||||
});
|
||||
};
|
||||
|
||||
const recent = (n = 10) => db.prepare('SELECT * FROM sessions ORDER BY ended_at DESC LIMIT ?').all(n);
|
||||
const sessions = (optsOrN) => {
|
||||
const opts = normalizeOpts(optsOrN, 'sessionId');
|
||||
const { limit = 50 } = opts;
|
||||
const { where, params } = buildWhere(opts, { sessionId: 's.id', project: 's.project', timestamp: 's.started_at', branch: 's.git_branch' });
|
||||
params.push(limit);
|
||||
return db.prepare(`SELECT * FROM sessions s WHERE ${where} ORDER BY ended_at DESC LIMIT ?`).all(...params);
|
||||
};
|
||||
|
||||
const summaries = (sessionId) => sessionId
|
||||
? db.prepare('SELECT * FROM summaries WHERE session_id=? ORDER BY timestamp').all(sessionId)
|
||||
: db.prepare('SELECT su.*, s.title as session_title, s.project FROM summaries su LEFT JOIN sessions s ON s.id=su.session_id ORDER BY su.timestamp DESC').all();
|
||||
const recent = (n = 10) => sessions({ limit: n });
|
||||
|
||||
const summaries = (optsOrSid) => {
|
||||
const opts = normalizeOpts(optsOrSid);
|
||||
const { limit = 100 } = opts;
|
||||
const { where, params } = buildWhere(opts, { sessionId: 'su.session_id', project: 's.project', timestamp: 'su.timestamp', branch: 's.git_branch' });
|
||||
params.push(limit);
|
||||
return db.prepare(`SELECT su.*, s.title as session_title, s.project FROM summaries su LEFT JOIN sessions s ON s.id=su.session_id WHERE ${where} ORDER BY su.timestamp DESC LIMIT ?`).all(...params);
|
||||
};
|
||||
|
||||
const resolveJsonlPath = (messageUuid) => {
|
||||
const msg = db.prepare('SELECT session_id, agent_id FROM messages WHERE uuid=?').get(messageUuid);
|
||||
@@ -153,7 +208,7 @@ function createQueryApi(db) {
|
||||
};
|
||||
};
|
||||
|
||||
return { sql: q, search, context, trace, thread, subagents, workflows, workflowTree, fileHistory, failures, recent, summaries, raw };
|
||||
return { sql: q, search, context, trace, thread, subagents, workflows, workflowTree, fileHistory, failures, sessions, recent, summaries, raw };
|
||||
}
|
||||
|
||||
export { createQueryApi };
|
||||
|
||||
Reference in New Issue
Block a user