Files
claude-mem/src/services/sqlite/observations/get.ts
T
Alex Newman 753a993647 fix: address PR #1641 review comments (round 3)
- Fix migration version conflict: addSessionPlatformSourceColumn now uses v25
- Sanitize observation titles in file-context deny reason (strip newlines, limit length)
- Guard json_each() with LIKE '[%' check for legacy bare-path rows
- Guard /stream SSE endpoint with 503 before DB initialization
- Scope bun-runner signal exit handling to start subcommand only
- Normalize platformSource at route boundary in DataRoutes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 14:16:41 -07:00

153 lines
4.4 KiB
TypeScript

/**
* Observation retrieval functions
* Extracted from SessionStore.ts for modular organization
*/
import { Database } from 'bun:sqlite';
import { logger } from '../../../utils/logger.js';
import type { ObservationRecord } from '../../../types/database.js';
import type { GetObservationsByIdsOptions, ObservationSessionRow } from './types.js';
/**
* Get a single observation by ID
*/
export function getObservationById(db: Database, id: number): ObservationRecord | null {
const stmt = db.prepare(`
SELECT *
FROM observations
WHERE id = ?
`);
return stmt.get(id) as ObservationRecord | undefined || null;
}
/**
* Get observations by array of IDs with ordering and limit
*/
export function getObservationsByIds(
db: Database,
ids: number[],
options: GetObservationsByIdsOptions = {}
): ObservationRecord[] {
if (ids.length === 0) return [];
const { orderBy = 'date_desc', limit, project, type, concepts, files } = options;
const orderClause = orderBy === 'date_asc' ? 'ASC' : 'DESC';
const limitClause = limit ? `LIMIT ${limit}` : '';
// Build placeholders for IN clause
const placeholders = ids.map(() => '?').join(',');
const params: any[] = [...ids];
const additionalConditions: string[] = [];
// Apply project filter
if (project) {
additionalConditions.push('project = ?');
params.push(project);
}
// Apply type filter
if (type) {
if (Array.isArray(type)) {
const typePlaceholders = type.map(() => '?').join(',');
additionalConditions.push(`type IN (${typePlaceholders})`);
params.push(...type);
} else {
additionalConditions.push('type = ?');
params.push(type);
}
}
// Apply concepts filter
if (concepts) {
const conceptsList = Array.isArray(concepts) ? concepts : [concepts];
const conceptConditions = conceptsList.map(() =>
'EXISTS (SELECT 1 FROM json_each(concepts) WHERE value = ?)'
);
params.push(...conceptsList);
additionalConditions.push(`(${conceptConditions.join(' OR ')})`);
}
// Apply files filter
if (files) {
const filesList = Array.isArray(files) ? files : [files];
const fileConditions = filesList.map(() => {
return '(EXISTS (SELECT 1 FROM json_each(files_read) WHERE value LIKE ?) OR EXISTS (SELECT 1 FROM json_each(files_modified) WHERE value LIKE ?))';
});
filesList.forEach(file => {
params.push(`%${file}%`, `%${file}%`);
});
additionalConditions.push(`(${fileConditions.join(' OR ')})`);
}
const whereClause = additionalConditions.length > 0
? `WHERE id IN (${placeholders}) AND ${additionalConditions.join(' AND ')}`
: `WHERE id IN (${placeholders})`;
const stmt = db.prepare(`
SELECT *
FROM observations
${whereClause}
ORDER BY created_at_epoch ${orderClause}
${limitClause}
`);
return stmt.all(...params) as ObservationRecord[];
}
/**
* Get observations for a specific session
*/
export function getObservationsForSession(
db: Database,
memorySessionId: string
): ObservationSessionRow[] {
const stmt = db.prepare(`
SELECT title, subtitle, type, prompt_number
FROM observations
WHERE memory_session_id = ?
ORDER BY created_at_epoch ASC
`);
return stmt.all(memorySessionId) as ObservationSessionRow[];
}
/**
* Get observations associated with a given file path, scoped to specific projects.
* Matches on the full file path (not just basename) to avoid cross-project collisions.
*/
export function getObservationsByFilePath(
db: Database,
filePath: string,
options?: { projects?: string[]; limit?: number }
): ObservationRecord[] {
const rawLimit = options?.limit;
const limit = Number.isInteger(rawLimit) && (rawLimit as number) > 0
? Math.min(rawLimit as number, 100)
: 15;
const params: (string | number)[] = [filePath, filePath];
let projectClause = '';
if (options?.projects?.length) {
const placeholders = options.projects.map(() => '?').join(',');
projectClause = `AND project IN (${placeholders})`;
params.push(...options.projects);
}
params.push(limit);
const stmt = db.prepare(`
SELECT *
FROM observations
WHERE (
(files_read LIKE '[%' AND EXISTS (SELECT 1 FROM json_each(files_read) WHERE value = ?))
OR (files_modified LIKE '[%' AND EXISTS (SELECT 1 FROM json_each(files_modified) WHERE value = ?))
)
${projectClause}
ORDER BY created_at_epoch DESC
LIMIT ?
`);
return stmt.all(...params) as ObservationRecord[];
}