All files / src/clients/ep committeeClient.ts

97.43% Statements 38/39
75.75% Branches 25/33
100% Functions 8/8
97.22% Lines 35/36

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 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156                                                                      295x           17x                     14x 14x 14x     4x 4x   1x               14x 14x 10x 10x       4x         4x               4x       4x   4x 4x 4x 3x     1x                                   14x 14x 14x 14x 13x 13x   1x         1x                       4x 4x   4x         3x 4x 4x      
/**
 * @fileoverview Committee sub-client for European Parliament API
 *
 * Handles committee (corporate body) information lookups including
 * direct lookup, list search, and current corporate bodies.
 *
 * @module clients/ep/committeeClient
 */
 
import { auditLogger, toErrorMessage } from '../../utils/auditLogger.js';
import type {
  Committee,
  PaginatedResponse,
} from '../../types/europeanParliament.js';
import {
  transformCorporateBody as _transformCorporateBody,
} from './transformers.js';
import {
  BaseEPClient,
  APIError,
  type EPClientConfig,
  type EPSharedResources,
  type JSONLDResponse,
} from './baseClient.js';
 
// ─── Committee Client ─────────────────────────────────────────────────────────
 
/**
 * Sub-client for committee/corporate-body EP API endpoints.
 *
 * @extends BaseEPClient
 * @public
 */
export class CommitteeClient extends BaseEPClient {
  constructor(config: EPClientConfig = {}, shared?: EPSharedResources) {
    super(config, shared);
  }
 
  // ─── Transform helpers ────────────────────────────────────────────────────
 
  private transformCorporateBody(apiData: Record<string, unknown>): Committee {
    return _transformCorporateBody(apiData);
  }
 
  // ─── Private helpers ──────────────────────────────────────────────────────
 
  /**
   * Resolves a committee by trying direct lookup then list search.
   * @throws {APIError} If committee not found
   * @private
   */
  private async resolveCommittee(searchTerm: string): Promise<Committee> {
    Eif (searchTerm !== '') {
      const directResult = await this.fetchCommitteeDirectly(searchTerm);
      if (directResult !== null) return directResult;
    }
 
    const found = await this.searchCommitteeInList(searchTerm);
    if (found !== null) return found;
 
    throw new APIError(`Committee not found: ${searchTerm || 'unknown'}`, 404);
  }
 
  /**
   * Attempts a direct corporate-body lookup by ID.
   * @private
   */
  private async fetchCommitteeDirectly(bodyId: string): Promise<Committee | null> {
    try {
      const response = await this.get<JSONLDResponse>(`corporate-bodies/${bodyId}`, {});
      Eif (response.data.length > 0) {
        return this.transformCorporateBody(response.data[0] ?? {});
      }
    } catch (error: unknown) {
      // A 404 from the direct lookup is an expected miss; don't log it as an error.
      Iif (!(error instanceof APIError && error.statusCode === 404)) {
        auditLogger.logError('get_committee_info.fetch_direct', { bodyId }, toErrorMessage(error));
      }
      // Body not found by direct lookup or unexpected failure already logged
    }
    return null;
  }
 
  /**
   * Searches the corporate-bodies list for a matching committee.
   * @private
   */
  private async searchCommitteeInList(searchTerm: string): Promise<Committee | null> {
    const listParams: Record<string, unknown> = {
      'body-classification': 'COMMITTEE_PARLIAMENTARY_STANDING',
      limit: 50,
    };
    const response = await this.get<JSONLDResponse>('corporate-bodies', listParams);
 
    for (const item of response.data) {
      const committee = this.transformCorporateBody(item);
      if (committee.abbreviation === searchTerm || committee.id === searchTerm) {
        return committee;
      }
    }
    return null;
  }
 
  // ─── Public methods ───────────────────────────────────────────────────────
 
  /**
   * Retrieves committee (corporate body) information by ID or abbreviation.
   *
   * **EP API Endpoint:** `GET /corporate-bodies/{body-id}` or `GET /corporate-bodies`
   *
   * @param params - id or abbreviation of the committee
   * @returns Committee information
   * @security Audit logged per GDPR Article 30
   */
  async getCommitteeInfo(params: {
    id?: string;
    abbreviation?: string;
  }): Promise<Committee> {
    const action = 'get_committee_info';
    try {
      const searchTerm = params.abbreviation ?? params.id ?? '';
      const committee = await this.resolveCommittee(searchTerm);
      auditLogger.logDataAccess(action, params, 1);
      return committee;
    } catch (error) {
      auditLogger.logError(
        action,
        params,
        error instanceof Error ? error.message : 'Unknown error'
      );
      throw error;
    }
  }
 
  /**
   * Returns the list of all current EP Corporate Bodies for today's date.
   * **EP API Endpoint:** `GET /corporate-bodies/show-current`
   */
  async getCurrentCorporateBodies(params: {
    limit?: number;
    offset?: number;
  } = {}): Promise<PaginatedResponse<Committee>> {
    const limit = params.limit ?? 50;
    const offset = params.offset ?? 0;
 
    const response = await this.get<JSONLDResponse>(
      'corporate-bodies/show-current',
      { format: 'application/ld+json', offset, limit }
    );
 
    const items = Array.isArray(response.data) ? response.data : [];
    const bodies = items.map((item) => this.transformCorporateBody(item));
    return { data: bodies, total: bodies.length + offset, limit, offset, hasMore: bodies.length === limit };
  }
}