Implement plan

This commit is contained in:
gpt-engineer-app[bot]
2025-10-19 19:28:04 +00:00
parent 5a138688bc
commit 5fa073cca2
3 changed files with 166 additions and 30 deletions

View File

@@ -472,6 +472,140 @@ export async function detectChanges(
};
}
/**
* ═══════════════════════════════════════════════════════════════════
* TYPE-SAFE CHANGE EXTRACTION FOR EDIT SUBMISSIONS
* ═══════════════════════════════════════════════════════════════════
*
* Extracts ONLY changed fields from form data compared to original entity data.
* Critical for edit operations to avoid passing unchanged fields through moderation.
*
* Benefits:
* ✅ Clearer audit trail (only see actual changes)
* ✅ Smaller database writes (no unnecessary updates)
* ✅ Correct validation (unchanged required fields stay in original_data)
* ✅ Type-safe with generics (compiler catches errors)
* ✅ Follows project knowledge: "only funnel through real changes"
*
* @param formData - New form data from user submission
* @param originalData - Original entity data from database
* @returns Object containing ONLY changed fields
*
* @example
* // Edit that only changes description
* extractChangedFields(
* { name: "Cedar Point", description: "New desc" },
* { name: "Cedar Point", description: "Old desc", location_id: "uuid-123" }
* )
* // Returns: { description: "New desc" }
* // ✅ location_id NOT included (unchanged, exists in original_data)
*/
export function extractChangedFields<T extends Record<string, any>>(
formData: T,
originalData: Partial<T>
): Partial<T> {
const changes: Partial<T> = {};
// Critical IDs that MUST always be included for relational integrity
// Even if "unchanged", these maintain foreign key relationships
const alwaysIncludeIds = [
'park_id', // Rides belong to parks
'ride_id', // For ride updates
'company_id', // For company updates
'manufacturer_id', // Rides reference manufacturers
'ride_model_id', // Rides reference models
'operator_id', // Parks reference operators
'property_owner_id', // Parks reference property owners
'designer_id', // Rides reference designers
];
Object.keys(formData).forEach((key) => {
const newValue = formData[key];
const oldValue = originalData[key];
// Always include critical relational IDs (even if unchanged)
if (alwaysIncludeIds.includes(key)) {
if (newValue !== undefined && newValue !== null) {
changes[key as keyof T] = newValue;
}
return;
}
// Skip system fields and fields that shouldn't be tracked
if (!shouldTrackField(key)) {
return;
}
// ═══ SPECIAL HANDLING: LOCATION OBJECTS ═══
// Location can be an object (from form) vs location_id (from DB)
if (key === 'location' && newValue && typeof newValue === 'object') {
const oldLoc = originalData.location as any;
if (!oldLoc || !isEqual(oldLoc, newValue)) {
changes[key as keyof T] = newValue;
}
return;
}
// ═══ SPECIAL HANDLING: DATE FIELDS WITH PRECISION ═══
// opening_date, closing_date, founded_date, etc.
if (key.endsWith('_date') && !key.endsWith('_precision')) {
const precisionKey = `${key}_precision` as keyof T;
const newDate = newValue;
const oldDate = oldValue;
const newPrecision = formData[precisionKey];
const oldPrecision = originalData[precisionKey];
// Include if EITHER date OR precision changed
if (!isEqual(newDate, oldDate) || !isEqual(newPrecision, oldPrecision)) {
changes[key as keyof T] = newValue;
// Also include precision if it exists
if (newPrecision !== undefined) {
changes[precisionKey] = newPrecision;
}
}
return;
}
// Skip precision fields (they're handled with their date above)
if (key.endsWith('_precision')) {
return;
}
// ═══ SPECIAL HANDLING: IMAGE FIELDS ═══
// Images have their own assignment system and should always be included if present
if (key === 'images' || key.includes('image_')) {
if (!isEqual(newValue, oldValue)) {
changes[key as keyof T] = newValue;
}
return;
}
// ═══ GENERAL FIELD COMPARISON ═══
// Include field if:
// 1. Value changed from something to something else
// 2. Value added (old was empty, new has value)
//
// Do NOT include if:
// 1. Both values are empty (null, undefined, '')
// 2. Values are equal after normalization
const oldEmpty = oldValue === null || oldValue === undefined || oldValue === '';
const newEmpty = newValue === null || newValue === undefined || newValue === '';
// If both empty, don't track (no change)
if (oldEmpty && newEmpty) {
return;
}
// If values differ, include the change
if (!isEqual(oldValue, newValue)) {
changes[key as keyof T] = newValue;
}
});
return changes;
}
/**
* Determines if a field should be tracked for changes
*/