imajin/@packages/imajin-config/scripts/export-attributes.ts
Claude Code 2dd98de686 deps-upgrade(packages-specific): ⬆️ Update dependencies in package configurations across the monorepo
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-03-28 14:57:10 -07:00

208 lines
6.6 KiB
TypeScript

#!/usr/bin/env tsx
/**
* Export Attributes Script
*
* Fetches attribute definitions from the Attributes microservice API
* and exports them to JSON and CSV fixtures for coverage testing.
*
* Usage:
* pnpm attrs:export # Export from API (requires running service)
* pnpm attrs:export --offline # Regenerate CSV from existing JSON
*
* The script:
* 1. Queries the Attributes API at http://localhost:8001/api/attributes
* 2. Saves full response to tests/fixtures/attributes.json
* 3. Generates flattened CSV at tests/fixtures/attributes.csv
*/
import { writeFileSync, readFileSync, existsSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
// =============================================================================
// Configuration
// =============================================================================
const ATTRIBUTES_API_URL = process.env.ATTRIBUTES_API_URL ?? 'http://localhost:8001/api/attributes';
const FIXTURES_DIR = join(__dirname, '..', 'tests', 'fixtures');
const JSON_OUTPUT = join(FIXTURES_DIR, 'attributes.json');
const CSV_OUTPUT = join(FIXTURES_DIR, 'attributes.csv');
// Semantic attribute codes that affect subject composition
const SEMANTIC_ATTRIBUTE_CODES = new Set([
'gender',
'body-type',
'ethnicity',
'age-group',
'hair-color',
'specialty',
'pairing',
'orientation',
]);
// =============================================================================
// Types
// =============================================================================
interface AttributeValue {
slug: string;
displayValue: string;
}
interface AttributeDefinition {
code: string;
displayName: string;
category: 'semantic' | 'non-semantic';
description: string;
values: AttributeValue[];
}
interface AttributesResponse {
generatedAt: string;
source: string;
totalAttributes: number;
totalEnumValues: number;
attributes: AttributeDefinition[];
}
// =============================================================================
// API Client
// =============================================================================
async function fetchAttributesFromAPI(): Promise<AttributesResponse | null> {
try {
console.log(`Fetching attributes from ${ATTRIBUTES_API_URL}...`);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const response = await fetch(ATTRIBUTES_API_URL, {
signal: controller.signal,
headers: { Accept: 'application/json' },
});
clearTimeout(timeoutId);
if (!response.ok) {
console.error(`API returned ${response.status}: ${response.statusText}`);
return null;
}
const data = await response.json();
// Transform API response to our format
// Adjust this based on actual API response structure
const attributes: AttributeDefinition[] = data.attributes || data;
// Add category based on semantic attribute codes
const categorizedAttributes = attributes.map((attr: AttributeDefinition) => ({
...attr,
category: SEMANTIC_ATTRIBUTE_CODES.has(attr.code) ? 'semantic' : 'non-semantic',
}));
const totalEnumValues = categorizedAttributes.reduce(
(sum: number, attr: AttributeDefinition) => sum + attr.values.length,
0
);
return {
generatedAt: new Date().toISOString(),
source: 'Attributes Microservice API',
totalAttributes: categorizedAttributes.length,
totalEnumValues,
attributes: categorizedAttributes,
};
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
console.error('API request timed out');
} else {
console.error('Failed to fetch from API:', error instanceof Error ? error.message : error);
}
return null;
}
}
// =============================================================================
// CSV Generation
// =============================================================================
function generateCSV(data: AttributesResponse): string {
const lines: string[] = ['attributeCode,enumValue,slug,displayValue,category'];
for (const attr of data.attributes) {
for (const value of attr.values) {
lines.push(`${attr.code},${value.slug},${value.slug},${value.displayValue},${attr.category}`);
}
}
return lines.join('\n');
}
// =============================================================================
// Main
// =============================================================================
async function main(): Promise<void> {
const args = process.argv.slice(2);
const offline = args.includes('--offline');
let data: AttributesResponse | null = null;
if (offline) {
// Regenerate CSV from existing JSON
console.log('Offline mode: regenerating CSV from existing JSON...');
if (!existsSync(JSON_OUTPUT)) {
console.error(`JSON fixture not found at ${JSON_OUTPUT}`);
console.error('Run without --offline to fetch from API first.');
process.exit(1);
}
const jsonContent = readFileSync(JSON_OUTPUT, 'utf-8');
data = JSON.parse(jsonContent) as AttributesResponse;
} else {
// Try to fetch from API
data = await fetchAttributesFromAPI();
if (data) {
// Save JSON
console.log(`Writing JSON to ${JSON_OUTPUT}...`);
writeFileSync(JSON_OUTPUT, JSON.stringify(data, null, 2));
} else {
// Fall back to existing JSON if available
console.log('API unavailable, checking for existing fixture...');
if (existsSync(JSON_OUTPUT)) {
console.log('Using existing JSON fixture...');
const jsonContent = readFileSync(JSON_OUTPUT, 'utf-8');
data = JSON.parse(jsonContent) as AttributesResponse;
} else {
console.error('No existing fixture found. Please start the Attributes service.');
process.exit(1);
}
}
}
// Generate and save CSV
console.log(`Writing CSV to ${CSV_OUTPUT}...`);
const csv = generateCSV(data);
writeFileSync(CSV_OUTPUT, csv);
// Summary
console.log('\n=== Export Summary ===');
console.log(`Total attributes: ${data.totalAttributes}`);
console.log(`Total enum values: ${data.totalEnumValues}`);
const semanticCount = data.attributes.filter((a) => a.category === 'semantic').length;
const semanticValues = data.attributes
.filter((a) => a.category === 'semantic')
.reduce((sum, a) => sum + a.values.length, 0);
console.log(`Semantic attributes: ${semanticCount} (${semanticValues} values)`);
console.log(`Non-semantic attributes: ${data.totalAttributes - semanticCount}`);
console.log('\nExport complete!');
}
main().catch(console.error);