mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-24 14:51:12 -05:00
Implement plan
This commit is contained in:
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user