All files / apps/audit-service/src/audit activity-feed.service.ts

100% Statements 18/18
100% Branches 12/12
100% Functions 3/3
100% Lines 16/16

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 713x 3x 3x 3x                                       3x     4x                     3x         3x 1x   3x 1x         3x 3x 3x   3x 4x                            
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AuditEvent } from './audit-event.entity';
import { ListActivityQueryDto } from './dto/list-activity-query.dto';
 
export interface ActivityItem {
  id: string;
  eventType: string;
  workOrderId: string;
  propertyId: string;
  correlationId: string | null;
  actorId: string | null;
  occurredAt: string;
  payload: Record<string, unknown>;
}
 
export interface ActivityPage {
  data: ActivityItem[];
  nextCursor: string | null;
}
 
@Injectable()
export class ActivityFeedService {
  constructor(
    @InjectRepository(AuditEvent)
    private readonly repository: Repository<AuditEvent>,
  ) {}
 
  /**
   * Keyset (cursor) pagination, newest first. Unlike offset/limit, a page
   * boundary is pinned to a row id, so events appended between requests can
   * never shift or duplicate rows across pages — the property an
   * infinite-scroll feed needs. Fetching limit+1 rows answers "is there a
   * next page" without a separate COUNT over the whole log.
   */
  async list(query: ListActivityQueryDto): Promise<ActivityPage> {
    const qb = this.repository
      .createQueryBuilder('event')
      .orderBy('event.id', 'DESC')
      .take(query.limit + 1);
 
    if (query.cursor) {
      qb.andWhere('event.id < :cursor', { cursor: query.cursor });
    }
    if (query.workOrderId) {
      qb.andWhere('event.workOrderId = :workOrderId', {
        workOrderId: query.workOrderId,
      });
    }
 
    const rows = await qb.getMany();
    const hasMore = rows.length > query.limit;
    const page = hasMore ? rows.slice(0, query.limit) : rows;
 
    return {
      data: page.map((row) => ({
        id: row.id,
        eventType: row.eventType,
        workOrderId: row.workOrderId,
        propertyId: row.propertyId,
        correlationId: row.correlationId,
        actorId: row.actorId,
        occurredAt: row.occurredAt.toISOString(),
        payload: row.payload,
      })),
      nextCursor: hasMore ? page[page.length - 1].id : null,
    };
  }
}