mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-22 18:30:09 +00:00
fix(migrations): ensure unique IDs and prevent data loss in split operations
When splitting one operation into misc and tasks operations: - Both operations now get unique ID suffixes (_misc and _tasks) - Skip logic verifies both conditions before skipping: 1. tasks config has migrated field (isConfirmBeforeDelete) 2. misc config has NO migrated fields remaining This prevents: - Potential operation ID conflicts in the sync log - Data loss in partial migration scenarios Adds test cases for partial migration and correct skip scenarios.
This commit is contained in:
parent
c42d5ace43
commit
be4b8ba241
2 changed files with 44 additions and 6 deletions
|
|
@ -30,8 +30,8 @@ export const MiscToTasksSettingsMigration_v1v2: SchemaMigration = {
|
|||
|
||||
const tasks = state.globalConfig?.tasks ?? {};
|
||||
|
||||
// Skip if already migrated (tasks has new fields)
|
||||
if (tasks.isConfirmBeforeDelete !== undefined) {
|
||||
// Skip if already migrated (tasks has new fields AND misc has no migrated fields)
|
||||
if (tasks.isConfirmBeforeDelete !== undefined && !hasMigratedFields(misc)) {
|
||||
return state;
|
||||
}
|
||||
|
||||
|
|
@ -94,13 +94,17 @@ export const MiscToTasksSettingsMigration_v1v2: SchemaMigration = {
|
|||
const result: OperationLike[] = [];
|
||||
|
||||
if (Object.keys(miscCfg).length > 0) {
|
||||
result.push({ ...op, payload: buildPayload(miscCfg, 'misc') });
|
||||
result.push({
|
||||
...op,
|
||||
id: `${op.id}_misc`,
|
||||
payload: buildPayload(miscCfg, 'misc'),
|
||||
});
|
||||
}
|
||||
|
||||
if (Object.keys(tasksCfg).length > 0) {
|
||||
result.push({
|
||||
...op,
|
||||
id: `${op.id}_tasks_migrated`,
|
||||
id: `${op.id}_tasks`,
|
||||
entityId: 'tasks',
|
||||
payload: buildPayload(tasksCfg, 'tasks'),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -64,14 +64,46 @@ describe('Migrate MiscConfig to TasksConfig', () => {
|
|||
expect(migratedState).toEqual(initialState);
|
||||
});
|
||||
|
||||
it('should not modify state if tasks already migrated', () => {
|
||||
it('should complete migration even if tasks.isConfirmBeforeDelete exists but misc still has migrated fields', () => {
|
||||
const initialState = {
|
||||
globalConfig: {
|
||||
misc: {
|
||||
isConfirmBeforeTaskDelete: true,
|
||||
defaultProjectId: 'proj-123',
|
||||
someOtherField: 'value',
|
||||
},
|
||||
tasks: {
|
||||
isConfirmBeforeDelete: false, // Already exists
|
||||
existingField: 'existing',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const migratedState = migration.migrateState(initialState) as {
|
||||
globalConfig: {
|
||||
misc: Record<string, unknown>;
|
||||
tasks: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
|
||||
// Should migrate remaining fields from misc
|
||||
expect(migratedState.globalConfig.tasks.isConfirmBeforeDelete).toBe(true); // Migrated value overwrites
|
||||
expect(migratedState.globalConfig.tasks.defaultProjectId).toBe('proj-123'); // Migrated
|
||||
expect(migratedState.globalConfig.tasks.existingField).toBe('existing'); // Preserved
|
||||
expect(migratedState.globalConfig.misc.isConfirmBeforeTaskDelete).toBeUndefined(); // Removed
|
||||
expect(migratedState.globalConfig.misc.defaultProjectId).toBeUndefined(); // Removed
|
||||
expect(migratedState.globalConfig.misc.someOtherField).toBe('value'); // Preserved
|
||||
});
|
||||
|
||||
it('should skip migration if tasks already has migrated fields AND misc has no migrated fields', () => {
|
||||
const initialState = {
|
||||
globalConfig: {
|
||||
misc: {
|
||||
someOtherField: 'value', // Not a migrated field
|
||||
},
|
||||
tasks: {
|
||||
isConfirmBeforeDelete: true,
|
||||
defaultProjectId: 'proj-123',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -111,6 +143,7 @@ describe('Migrate MiscConfig to TasksConfig', () => {
|
|||
// First operation should be misc with non-migrated settings
|
||||
const miscOp = resultArray.find((r) => r.entityId === 'misc');
|
||||
expect(miscOp).toBeDefined();
|
||||
expect(miscOp!.id).toBe('op_1_misc');
|
||||
expect(miscOp!.payload).toEqual({
|
||||
isMinimizeToTray: false,
|
||||
});
|
||||
|
|
@ -118,7 +151,7 @@ describe('Migrate MiscConfig to TasksConfig', () => {
|
|||
// Second operation should be tasks with migrated settings
|
||||
const tasksOp = resultArray.find((r) => r.entityId === 'tasks');
|
||||
expect(tasksOp).toBeDefined();
|
||||
expect(tasksOp!.id).toBe('op_1_tasks_migrated');
|
||||
expect(tasksOp!.id).toBe('op_1_tasks');
|
||||
expect(tasksOp!.payload).toEqual({
|
||||
isConfirmBeforeDelete: true,
|
||||
isMarkdownFormattingInNotesEnabled: false, // Inverted from isTurnOffMarkdown: true
|
||||
|
|
@ -143,6 +176,7 @@ describe('Migrate MiscConfig to TasksConfig', () => {
|
|||
// Should return single tasks operation
|
||||
expect(Array.isArray(result)).toBe(false);
|
||||
const singleOp = result as OperationLike;
|
||||
expect(singleOp.id).toBe('op_2_tasks');
|
||||
expect(singleOp.entityId).toBe('tasks');
|
||||
expect(singleOp.payload).toEqual({
|
||||
isAutoAddWorkedOnToToday: true,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue