mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
feat: Refactor feature export service with type guards and parallel conflict checking
This commit is contained in:
@@ -158,45 +158,40 @@ export function createConflictCheckHandler(featureLoader: FeatureLoader) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract features from the data
|
// Extract features from the data using type guards
|
||||||
type FeatureExportType = { feature: { id: string; title?: string } };
|
|
||||||
type BulkExportType = { features: FeatureExportType[] };
|
|
||||||
type RawFeatureType = { id: string; title?: string };
|
|
||||||
|
|
||||||
let featuresToCheck: Array<{ id: string; title?: string }> = [];
|
let featuresToCheck: Array<{ id: string; title?: string }> = [];
|
||||||
|
|
||||||
if ('features' in parsed && Array.isArray((parsed as BulkExportType).features)) {
|
if (exportService.isBulkExport(parsed)) {
|
||||||
// Bulk export format
|
// Bulk export format
|
||||||
featuresToCheck = (parsed as BulkExportType).features.map((f) => ({
|
featuresToCheck = parsed.features.map((f) => ({
|
||||||
id: f.feature.id,
|
id: f.feature.id,
|
||||||
title: f.feature.title,
|
title: f.feature.title,
|
||||||
}));
|
}));
|
||||||
} else if ('feature' in parsed) {
|
} else if (exportService.isFeatureExport(parsed)) {
|
||||||
// Single FeatureExport format
|
// Single FeatureExport format
|
||||||
const featureExport = parsed as FeatureExportType;
|
|
||||||
featuresToCheck = [
|
featuresToCheck = [
|
||||||
{
|
{
|
||||||
id: featureExport.feature.id,
|
id: parsed.feature.id,
|
||||||
title: featureExport.feature.title,
|
title: parsed.feature.title,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
} else if ('id' in parsed) {
|
} else if (exportService.isRawFeature(parsed)) {
|
||||||
// Raw Feature format
|
// Raw Feature format
|
||||||
const rawFeature = parsed as RawFeatureType;
|
featuresToCheck = [{ id: parsed.id, title: parsed.title }];
|
||||||
featuresToCheck = [{ id: rawFeature.id, title: rawFeature.title }];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check each feature for conflicts
|
// Check each feature for conflicts in parallel
|
||||||
const conflicts: ConflictInfo[] = [];
|
const conflicts: ConflictInfo[] = await Promise.all(
|
||||||
for (const feature of featuresToCheck) {
|
featuresToCheck.map(async (feature) => {
|
||||||
const existing = await featureLoader.get(projectPath, feature.id);
|
const existing = await featureLoader.get(projectPath, feature.id);
|
||||||
conflicts.push({
|
return {
|
||||||
featureId: feature.id,
|
featureId: feature.id,
|
||||||
title: feature.title,
|
title: feature.title,
|
||||||
existingTitle: existing?.title,
|
existingTitle: existing?.title,
|
||||||
hasConflict: !!existing,
|
hasConflict: !!existing,
|
||||||
});
|
};
|
||||||
}
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const hasConflicts = conflicts.some((c) => c.hasConflict);
|
const hasConflicts = conflicts.some((c) => c.hasConflict);
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ export class FeatureExportService {
|
|||||||
...(metadata ? { metadata } : {}),
|
...(metadata ? { metadata } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.serializeExport(exportData, format, prettyPrint);
|
return this.serialize(exportData, format, prettyPrint);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -170,16 +170,19 @@ export class FeatureExportService {
|
|||||||
features = features.filter((f) => f.status === status);
|
features = features.filter((f) => f.status === status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate timestamp once for consistent export time across all features
|
||||||
|
const exportedAt = new Date().toISOString();
|
||||||
|
|
||||||
// Prepare feature exports
|
// Prepare feature exports
|
||||||
const featureExports: FeatureExport[] = features.map((feature) => ({
|
const featureExports: FeatureExport[] = features.map((feature) => ({
|
||||||
version: FEATURE_EXPORT_VERSION,
|
version: FEATURE_EXPORT_VERSION,
|
||||||
feature: this.prepareFeatureForExport(feature, { includeHistory, includePlanSpec }),
|
feature: this.prepareFeatureForExport(feature, { includeHistory, includePlanSpec }),
|
||||||
exportedAt: new Date().toISOString(),
|
exportedAt,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const bulkExport: BulkExportResult = {
|
const bulkExport: BulkExportResult = {
|
||||||
version: FEATURE_EXPORT_VERSION,
|
version: FEATURE_EXPORT_VERSION,
|
||||||
exportedAt: new Date().toISOString(),
|
exportedAt,
|
||||||
count: featureExports.length,
|
count: featureExports.length,
|
||||||
features: featureExports,
|
features: featureExports,
|
||||||
...(metadata ? { metadata } : {}),
|
...(metadata ? { metadata } : {}),
|
||||||
@@ -187,7 +190,7 @@ export class FeatureExportService {
|
|||||||
|
|
||||||
logger.info(`Exported ${featureExports.length} features from ${projectPath}`);
|
logger.info(`Exported ${featureExports.length} features from ${projectPath}`);
|
||||||
|
|
||||||
return this.serializeBulkExport(bulkExport, format, prettyPrint);
|
return this.serialize(bulkExport, format, prettyPrint);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -449,7 +452,7 @@ export class FeatureExportService {
|
|||||||
/**
|
/**
|
||||||
* Check if parsed data is a bulk export
|
* Check if parsed data is a bulk export
|
||||||
*/
|
*/
|
||||||
private isBulkExport(data: unknown): data is BulkExportResult {
|
isBulkExport(data: unknown): data is BulkExportResult {
|
||||||
if (!data || typeof data !== 'object') {
|
if (!data || typeof data !== 'object') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -457,6 +460,36 @@ export class FeatureExportService {
|
|||||||
return 'version' in obj && 'features' in obj && Array.isArray(obj.features);
|
return 'version' in obj && 'features' in obj && Array.isArray(obj.features);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if parsed data is a single FeatureExport
|
||||||
|
*/
|
||||||
|
isFeatureExport(data: unknown): data is FeatureExport {
|
||||||
|
if (!data || typeof data !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const obj = data as Record<string, unknown>;
|
||||||
|
return (
|
||||||
|
'version' in obj &&
|
||||||
|
'feature' in obj &&
|
||||||
|
'exportedAt' in obj &&
|
||||||
|
typeof obj.feature === 'object' &&
|
||||||
|
obj.feature !== null &&
|
||||||
|
'id' in (obj.feature as Record<string, unknown>)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if parsed data is a raw Feature
|
||||||
|
*/
|
||||||
|
isRawFeature(data: unknown): data is Feature {
|
||||||
|
if (!data || typeof data !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const obj = data as Record<string, unknown>;
|
||||||
|
// A raw feature has 'id' but not the 'version' + 'feature' wrapper of FeatureExport
|
||||||
|
return 'id' in obj && !('feature' in obj && 'version' in obj);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate a feature has required fields
|
* Validate a feature has required fields
|
||||||
*/
|
*/
|
||||||
@@ -475,24 +508,10 @@ export class FeatureExportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serialize export data to string
|
* Serialize export data to string (handles both single feature and bulk exports)
|
||||||
*/
|
*/
|
||||||
private serializeExport(data: FeatureExport, format: ExportFormat, prettyPrint: boolean): string {
|
private serialize<T extends FeatureExport | BulkExportResult>(
|
||||||
if (format === 'yaml') {
|
data: T,
|
||||||
return yamlStringify(data, {
|
|
||||||
indent: 2,
|
|
||||||
lineWidth: 120,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return prettyPrint ? JSON.stringify(data, null, 2) : JSON.stringify(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serialize bulk export data to string
|
|
||||||
*/
|
|
||||||
private serializeBulkExport(
|
|
||||||
data: BulkExportResult,
|
|
||||||
format: ExportFormat,
|
format: ExportFormat,
|
||||||
prettyPrint: boolean
|
prettyPrint: boolean
|
||||||
): string {
|
): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user