@lineai/gov-deals
Version:
Explore Federal contracts for government building renovations, city hall renovations, courthouse updates, library modernizations, federal building improvement contracts, base housing and facilities upgrades.
130 lines • 9.08 kB
JavaScript
/**
* CSV-based data source for opportunities
* Provides same interface as SAM.gov API but reads from local CSV file
*/
import { promises as fs } from 'fs';
import { parse } from 'csv-parse/sync';
import { mapCsvToSamOpportunity, filterCsvRow } from './field-mapping';
import { SamOpportunitySearchResponseSchema, } from '../../types/opportunities';
import { ValidationError } from '../../core/errors';
/**
* CSV-based client that mimics SAM.gov API
*/
export class CsvClient {
csvPath;
cache = null;
cacheResults;
encoding;
constructor(options) {
if (!options.csvPath) {
throw new Error('CSV file path is required');
}
this.csvPath = options.csvPath;
this.cacheResults = options.cacheResults ?? true;
this.encoding = options.encoding ?? 'utf-8';
}
/**
* Load and parse CSV data
*/
async loadData() {
// Return cached data if available
if (this.cache && this.cacheResults) {
return this.cache;
}
try {
// Read CSV file
const csvContent = await fs.readFile(this.csvPath, this.encoding);
// Parse CSV with proper options
const records = parse(csvContent, {
columns: true,
skip_empty_lines: true,
relax_quotes: true,
relax_column_count: true,
skip_records_with_error: true, // Skip problematic rows
});
// Cache if enabled
if (this.cacheResults) {
this.cache = records;
}
return records;
}
catch (error) {
throw new Error(`Failed to load CSV file: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Search opportunities with filtering and pagination
*/
async search(filters, pagination) {
// Load all data
const allRecords = await this.loadData();
// Apply filters
let filteredRecords = allRecords;
if (filters) {
filteredRecords = allRecords.filter(row => filterCsvRow(row, filters));
}
// Calculate pagination
const limit = pagination?.limit || 100;
const page = pagination?.page || 1;
const offset = (page - 1) * limit;
// Get paginated results
const paginatedRecords = filteredRecords.slice(offset, offset + limit);
// Transform to SAM format
const opportunities = paginatedRecords.map(mapCsvToSamOpportunity);
// Build response
const response = {
totalRecords: filteredRecords.length,
limit,
offset,
opportunitiesData: opportunities,
links: [], // CSV doesn't have links
};
// Validate response format
const result = SamOpportunitySearchResponseSchema.safeParse(response);
if (!result.success) {
console.error('CSV mapping validation errors:', result.error.errors);
// Continue anyway - the data is still useful even if not perfectly typed
}
return response;
}
/**
* Get opportunity by ID
*/
async getById(noticeId) {
const allRecords = await this.loadData();
const record = allRecords.find(row => row.NoticeId === noticeId);
if (!record) {
throw new ValidationError(`Opportunity with ID ${noticeId} not found`);
}
return mapCsvToSamOpportunity(record);
}
/**
* Get opportunity description (from CSV Description field)
*/
async getDescription(noticeId) {
const allRecords = await this.loadData();
const record = allRecords.find(row => row.NoticeId === noticeId);
if (!record) {
throw new ValidationError(`Opportunity with ID ${noticeId} not found`);
}
return record.Description || 'No description available';
}
/**
* Search for construction opportunities
*/
async searchConstruction(additionalFilters, pagination) {
const constructionFilters = {
naicsCodes: ['236', '238'],
keywords: 'renovation construction building modernization',
...additionalFilters,
};
return this.search(constructionFilters, pagination);
}
/**
* Clear cache if needed
*/
clearCache() {
this.cache = null;
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xpZW50LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL2RhdGFzb3VyY2VzL2Nzdi9jbGllbnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7OztHQUdHO0FBRUgsT0FBTyxFQUFFLFFBQVEsSUFBSSxFQUFFLEVBQUUsTUFBTSxJQUFJLENBQUM7QUFDcEMsT0FBTyxFQUFFLEtBQUssRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBQ3ZDLE9BQU8sRUFFTCxzQkFBc0IsRUFDdEIsWUFBWSxFQUNiLE1BQU0saUJBQWlCLENBQUM7QUFDekIsT0FBTyxFQUlMLGtDQUFrQyxHQUNuQyxNQUFNLDJCQUEyQixDQUFDO0FBRW5DLE9BQU8sRUFBRSxlQUFlLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQztBQVFwRDs7R0FFRztBQUNILE1BQU0sT0FBTyxTQUFTO0lBQ1osT0FBTyxDQUFTO0lBQ2hCLEtBQUssR0FBK0IsSUFBSSxDQUFDO0lBQ3pDLFlBQVksQ0FBVTtJQUN0QixRQUFRLENBQWlCO0lBRWpDLFlBQVksT0FBeUI7UUFDbkMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxPQUFPLEVBQUU7WUFDcEIsTUFBTSxJQUFJLEtBQUssQ0FBQywyQkFBMkIsQ0FBQyxDQUFDO1NBQzlDO1FBRUQsSUFBSSxDQUFDLE9BQU8sR0FBRyxPQUFPLENBQUMsT0FBTyxDQUFDO1FBQy9CLElBQUksQ0FBQyxZQUFZLEdBQUcsT0FBTyxDQUFDLFlBQVksSUFBSSxJQUFJLENBQUM7UUFDakQsSUFBSSxDQUFDLFFBQVEsR0FBRyxPQUFPLENBQUMsUUFBUSxJQUFJLE9BQU8sQ0FBQztJQUM5QyxDQUFDO0lBRUQ7O09BRUc7SUFDSyxLQUFLLENBQUMsUUFBUTtRQUNwQixrQ0FBa0M7UUFDbEMsSUFBSSxJQUFJLENBQUMsS0FBSyxJQUFJLElBQUksQ0FBQyxZQUFZLEVBQUU7WUFDbkMsT0FBTyxJQUFJLENBQUMsS0FBSyxDQUFDO1NBQ25CO1FBRUQsSUFBSTtZQUNG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