export interface FieldDiff { key: string before: unknown after: unknown } export interface RowDiff { status: 'added' | 'removed' | 'changed' | 'unchanged' id?: string | number fields: FieldDiff[] before?: Record after?: Record } export interface TableDiff { tableName: string rows: RowDiff[] hasChanges: boolean } /** * Diff two arrays of row objects. Matches rows by `id` field if present, * otherwise by array position. */ export function diffTable( tableName: string, before: unknown[], after: unknown[], ): TableDiff { const beforeArr = (before || []) as Record[] const afterArr = (after || []) as Record[] const rows: RowDiff[] = [] // Build lookup by id if available const hasIds = beforeArr.length > 0 && 'id' in (beforeArr[0] || {}) if (hasIds) { const beforeMap = new Map(beforeArr.map(r => [String(r.id), r])) const afterMap = new Map(afterArr.map(r => [String(r.id), r])) const allIds = new Set([...beforeMap.keys(), ...afterMap.keys()]) for (const id of allIds) { const b = beforeMap.get(id) const a = afterMap.get(id) if (!b && a) { rows.push({ status: 'added', id: a.id as string, fields: [], after: a }) } else if (b && !a) { rows.push({ status: 'removed', id: b.id as string, fields: [], before: b }) } else if (b && a) { const fields = diffFields(b, a) rows.push({ status: fields.length > 0 ? 'changed' : 'unchanged', id: b.id as string, fields, before: b, after: a, }) } } } else { // Positional comparison const maxLen = Math.max(beforeArr.length, afterArr.length) for (let i = 0; i < maxLen; i++) { const b = beforeArr[i] const a = afterArr[i] if (!b && a) { rows.push({ status: 'added', fields: [], after: a }) } else if (b && !a) { rows.push({ status: 'removed', fields: [], before: b }) } else if (b && a) { const fields = diffFields(b, a) rows.push({ status: fields.length > 0 ? 'changed' : 'unchanged', fields, before: b, after: a, }) } } } return { tableName, rows, hasChanges: rows.some(r => r.status !== 'unchanged'), } } function diffFields(before: Record, after: Record): FieldDiff[] { const diffs: FieldDiff[] = [] const allKeys = new Set([...Object.keys(before), ...Object.keys(after)]) for (const key of allKeys) { const bVal = JSON.stringify(before[key] ?? null) const aVal = JSON.stringify(after[key] ?? null) if (bVal !== aVal) { diffs.push({ key, before: before[key], after: after[key] }) } } return diffs }