@@ -54,6 +54,11 @@ interface MaxSeqRow {
5454 max_seq : number | null ;
5555}
5656
57+ // Default limits to prevent unbounded queries
58+ export const DEFAULT_FRAME_LIMIT = 1000 ;
59+ export const DEFAULT_EVENT_LIMIT = 500 ;
60+ export const DEFAULT_ANCHOR_LIMIT = 200 ;
61+
5762// Safe JSON parse with fallback
5863function safeJsonParse < T > ( json : string | null | undefined , fallback : T ) : T {
5964 if ( ! json ) return fallback ;
@@ -308,13 +313,17 @@ export class FrameDatabase {
308313 /**
309314 * Get frames by project and state
310315 */
311- getFramesByProject ( projectId : string , state ?: 'active' | 'closed' ) : Frame [ ] {
316+ getFramesByProject (
317+ projectId : string ,
318+ state ?: 'active' | 'closed' ,
319+ limit : number = DEFAULT_FRAME_LIMIT
320+ ) : Frame [ ] {
312321 try {
313322 const query = state
314- ? 'SELECT * FROM frames WHERE project_id = ? AND state = ? ORDER BY created_at'
315- : 'SELECT * FROM frames WHERE project_id = ? ORDER BY created_at' ;
323+ ? 'SELECT * FROM frames WHERE project_id = ? AND state = ? ORDER BY created_at LIMIT ? '
324+ : 'SELECT * FROM frames WHERE project_id = ? ORDER BY created_at LIMIT ? ' ;
316325
317- const params = state ? [ projectId , state ] : [ projectId ] ;
326+ const params = state ? [ projectId , state , limit ] : [ projectId , limit ] ;
318327 const rows = this . db . prepare ( query ) . all ( ...params ) as FrameRow [ ] ;
319328
320329 return rows . map ( ( row ) => ( {
@@ -397,13 +406,14 @@ export class FrameDatabase {
397406 /**
398407 * Get events for a frame
399408 */
400- getFrameEvents ( frameId : string , limit ?: number ) : Event [ ] {
409+ getFrameEvents (
410+ frameId : string ,
411+ limit : number = DEFAULT_EVENT_LIMIT
412+ ) : Event [ ] {
401413 try {
402- const query = limit
403- ? 'SELECT * FROM events WHERE frame_id = ? ORDER BY seq DESC LIMIT ?'
404- : 'SELECT * FROM events WHERE frame_id = ? ORDER BY seq ASC' ;
405-
406- const params = limit ? [ frameId , limit ] : [ frameId ] ;
414+ const query =
415+ 'SELECT * FROM events WHERE frame_id = ? ORDER BY seq DESC LIMIT ?' ;
416+ const params = [ frameId , limit ] ;
407417 const rows = this . db . prepare ( query ) . all ( ...params ) as EventRow [ ] ;
408418
409419 return rows . map ( ( row ) => ( {
@@ -500,13 +510,16 @@ export class FrameDatabase {
500510 /**
501511 * Get anchors for a frame
502512 */
503- getFrameAnchors ( frameId : string ) : Anchor [ ] {
513+ getFrameAnchors (
514+ frameId : string ,
515+ limit : number = DEFAULT_ANCHOR_LIMIT
516+ ) : Anchor [ ] {
504517 try {
505518 const rows = this . db
506519 . prepare (
507- 'SELECT * FROM anchors WHERE frame_id = ? ORDER BY priority DESC, created_at ASC'
520+ 'SELECT * FROM anchors WHERE frame_id = ? ORDER BY priority DESC, created_at ASC LIMIT ? '
508521 )
509- . all ( frameId ) as AnchorRow [ ] ;
522+ . all ( frameId , limit ) as AnchorRow [ ] ;
510523
511524 return rows . map ( ( row ) => ( {
512525 ...row ,
@@ -543,6 +556,34 @@ export class FrameDatabase {
543556 }
544557 }
545558
559+ /**
560+ * Count frames by project and state (without loading all data)
561+ */
562+ countFrames ( projectId ?: string , state ?: 'active' | 'closed' ) : number {
563+ try {
564+ let query = 'SELECT COUNT(*) as count FROM frames' ;
565+ const params : ( string | undefined ) [ ] = [ ] ;
566+
567+ if ( projectId ) {
568+ query += ' WHERE project_id = ?' ;
569+ params . push ( projectId ) ;
570+ if ( state ) {
571+ query += ' AND state = ?' ;
572+ params . push ( state ) ;
573+ }
574+ } else if ( state ) {
575+ query += ' WHERE state = ?' ;
576+ params . push ( state ) ;
577+ }
578+
579+ const result = this . db . prepare ( query ) . get ( ...params ) as CountRow ;
580+ return result . count ;
581+ } catch ( error : unknown ) {
582+ logger . warn ( 'Failed to count frames' , { error, projectId, state } ) ;
583+ return 0 ;
584+ }
585+ }
586+
546587 /**
547588 * Get database statistics
548589 */
0 commit comments