208 lines
6.6 KiB
TypeScript
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);
|