Skip to content

Commit 9702728

Browse files
authored
add logic for metadata migration (#218)
* [#215] Handle metadata migration Makes it possible to change "metadata keys" and the "app metadata key prefix" without losing data * [#215] Fix checking and handling missing reader settings in metadata With the new migration logic missing reader settings couldn't be detected anymore in case the metadata included outdated app metadata keys for the reader settings. * [#215] Fix name of "IReaderSettings" property
1 parent 7642b58 commit 9702728

10 files changed

+135
-22
lines changed

src/components/navbar/ReaderNavBar.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ export default function ReaderNavBar(props: IProps) {
256256
setSettingValue={updateSettingValue}
257257
staticNav={settings.staticNav}
258258
showPageNumber={settings.showPageNumber}
259-
loadNextonEnding={settings.loadNextonEnding}
259+
loadNextOnEnding={settings.loadNextOnEnding}
260260
readerType={settings.readerType}
261261
/>
262262
</Collapse>

src/components/reader/ReaderSettingsOptions.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ interface IProps extends IReaderSettings {
1919
}
2020

2121
export default function ReaderSettingsOptions({
22-
staticNav, loadNextonEnding, readerType, showPageNumber, setSettingValue,
22+
staticNav, loadNextOnEnding, readerType, showPageNumber, setSettingValue,
2323
}: IProps) {
2424
return (
2525
<>
@@ -49,8 +49,8 @@ export default function ReaderSettingsOptions({
4949
<ListItemSecondaryAction>
5050
<Switch
5151
edge="end"
52-
checked={loadNextonEnding}
53-
onChange={(e) => setSettingValue('loadNextonEnding', e.target.checked)}
52+
checked={loadNextOnEnding}
53+
onChange={(e) => setSettingValue('loadNextOnEnding', e.target.checked)}
5454
/>
5555
</ListItemSecondaryAction>
5656
</ListItem>

src/components/reader/pager/DoublePagedPager.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export default function DoublePagedPager(props: IReaderProps) {
102102
if (curPage < pages.length - 1) {
103103
const nextCurPage = curPage + pagesDisplayed.current;
104104
setCurPage((nextCurPage >= pages.length) ? pages.length - 1 : nextCurPage);
105-
} else if (settings.loadNextonEnding) {
105+
} else if (settings.loadNextOnEnding) {
106106
nextChapter();
107107
}
108108
}

src/components/reader/pager/HorizontalPager.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export default function HorizontalPager(props: IReaderProps) {
4242
if (curPage < pages.length - 1) {
4343
pagesRef.current[curPage + 1]?.scrollIntoView({ inline: 'center' });
4444
setCurPage((page) => page + 1);
45-
} else if (settings.loadNextonEnding) {
45+
} else if (settings.loadNextOnEnding) {
4646
nextChapter();
4747
}
4848
}
@@ -126,7 +126,7 @@ export default function HorizontalPager(props: IReaderProps) {
126126
}, [selfRef]);
127127

128128
useEffect(() => {
129-
if (settings.loadNextonEnding) {
129+
if (settings.loadNextOnEnding) {
130130
document.addEventListener('scroll', handleLoadNextonEnding);
131131
}
132132
selfRef.current?.addEventListener('mousedown', clickControl);

src/components/reader/pager/PagedPager.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export default function PagedReader(props: IReaderProps) {
2424
function nextPage() {
2525
if (curPage < pages.length - 1) {
2626
changePage(curPage + 1);
27-
} else if (settings.loadNextonEnding) {
27+
} else if (settings.loadNextOnEnding) {
2828
nextChapter();
2929
}
3030
}

src/components/reader/pager/VerticalPager.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export default function VerticalPager(props: IReaderProps) {
5555
setCurPage(currentPageRef.current);
5656

5757
// Go to next chapter if configured to and at bottom
58-
if (settings.loadNextonEnding) {
58+
if (settings.loadNextOnEnding) {
5959
nextChapter();
6060
}
6161
} else {
@@ -72,7 +72,7 @@ export default function VerticalPager(props: IReaderProps) {
7272
return () => {
7373
window.removeEventListener('scroll', handleScroll);
7474
};
75-
}, [settings.loadNextonEnding]);
75+
}, [settings.loadNextOnEnding]);
7676

7777
const go = useCallback((direction: 'up' | 'down') => {
7878
if (direction === 'down' && isAtBottom()) {

src/screens/settings/DefaultReaderSettings.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export default function DefaultReaderSettings() {
5353
setSettingValue={setSettingValue}
5454
staticNav={settings.staticNav}
5555
showPageNumber={settings.showPageNumber}
56-
loadNextonEnding={settings.loadNextonEnding}
56+
loadNextOnEnding={settings.loadNextOnEnding}
5757
readerType={settings.readerType}
5858
/>
5959
);

src/typings.d.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ interface IState {
5454
index: number
5555
}
5656

57+
interface IMetadataMigration {
58+
appKeyPrefix?: { oldPrefix: string, newPrefix: string }
59+
keys?: { oldKey: string, newKey: string }[]
60+
}
61+
5762
interface IMetadata<VALUES extends AllowedMetadataValueTypes = string> {
5863
[key: string]: VALUES;
5964
}
@@ -168,7 +173,7 @@ type ReaderType =
168173
interface IReaderSettings{
169174
staticNav: boolean
170175
showPageNumber: boolean
171-
loadNextonEnding: boolean
176+
loadNextOnEnding: boolean
172177
readerType: ReaderType
173178
}
174179

src/util/metadata.ts

+110-6
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,21 @@ import client from './client';
1111

1212
const APP_METADATA_KEY_PREFIX = 'webUI_';
1313

14-
const getMetadataKey = (key: string) => `${APP_METADATA_KEY_PREFIX}${key}`;
14+
const migrations: IMetadataMigration[] = [
15+
{
16+
keys: [
17+
{ oldKey: 'loadNextonEnding', newKey: 'loadNextOnEnding' },
18+
],
19+
},
20+
];
21+
22+
const getMetadataKey = (key: string, appPrefix: string = APP_METADATA_KEY_PREFIX) => `${appPrefix}${key}`;
23+
24+
const doesMetadataKeyExistIn = (
25+
meta: IMetadata | undefined,
26+
key: string,
27+
appPrefix?: string,
28+
): boolean => Object.prototype.hasOwnProperty.call(meta ?? {}, getMetadataKey(key, appPrefix));
1529

1630
const convertValueFromMetadata = <
1731
T extends AllowedMetadataValueTypes = AllowedMetadataValueTypes,
@@ -33,31 +47,121 @@ const convertValueFromMetadata = <
3347
return value as T;
3448
};
3549

50+
const getAppMetadataFrom = (
51+
meta: IMetadata,
52+
appPrefix: string = APP_METADATA_KEY_PREFIX,
53+
): IMetadata => {
54+
const appMetadata: IMetadata = {};
55+
56+
Object.entries(meta).forEach(([key, value]) => {
57+
if (key.startsWith(appPrefix)) {
58+
appMetadata[key] = value;
59+
}
60+
});
61+
62+
return appMetadata;
63+
};
64+
65+
const applyAppKeyPrefixMigration = (meta: IMetadata, migration: IMetadataMigration): IMetadata => {
66+
const migratedMetadata: IMetadata = { ...meta };
67+
68+
if (!migration.appKeyPrefix) {
69+
return migratedMetadata;
70+
}
71+
72+
const { oldPrefix, newPrefix } = migration.appKeyPrefix;
73+
74+
const oldAppMetadata = getAppMetadataFrom(meta, oldPrefix);
75+
const newAppMetadata = getAppMetadataFrom(meta, newPrefix);
76+
77+
const missingMetadataKeys = Object.keys(oldAppMetadata)
78+
.filter((key) => !Object.keys(newAppMetadata).includes(key));
79+
80+
const isMissingOldMetadata = missingMetadataKeys.length;
81+
if (isMissingOldMetadata) {
82+
missingMetadataKeys.forEach((oldKey) => {
83+
const keyWithNewPrefix = oldKey.replace(oldPrefix, newPrefix);
84+
migratedMetadata[keyWithNewPrefix] = oldAppMetadata[oldKey];
85+
});
86+
}
87+
88+
return migratedMetadata;
89+
};
90+
91+
const applyMetadataKeyMigration = (meta: IMetadata, migration: IMetadataMigration): IMetadata => {
92+
const migratedMetadata: IMetadata = { ...meta };
93+
94+
if (!migration.keys) {
95+
return migratedMetadata;
96+
}
97+
98+
const metadataKeyChanges = migration.keys;
99+
100+
metadataKeyChanges.forEach(({ oldKey, newKey }) => {
101+
if (!doesMetadataKeyExistIn(meta, oldKey)) {
102+
return;
103+
}
104+
105+
if (doesMetadataKeyExistIn(meta, newKey)) {
106+
return;
107+
}
108+
109+
migratedMetadata[getMetadataKey(newKey)] = meta[getMetadataKey(oldKey)];
110+
});
111+
112+
return migratedMetadata;
113+
};
114+
115+
const applyMetadataMigrations = (meta?: IMetadata): IMetadata | undefined => {
116+
if (!meta) {
117+
return undefined;
118+
}
119+
120+
const migrationToMetadata: [number, IMetadata][] = [[0, meta]];
121+
122+
migrations.forEach((migration, index) => {
123+
const migrationId = index + 1;
124+
const metadataToMigrate = migrationToMetadata[migrationId - 1][1];
125+
const appKeyPrefixMigrated = applyAppKeyPrefixMigration(metadataToMigrate, migration);
126+
const metadataKeysMigrated = applyMetadataKeyMigration(appKeyPrefixMigrated, migration);
127+
128+
migrationToMetadata.push([migrationId, metadataKeysMigrated]);
129+
});
130+
131+
const appliedMigration = migrationToMetadata.length > 1;
132+
if (!appliedMigration) {
133+
return { ...meta };
134+
}
135+
136+
return migrationToMetadata.pop()![1];
137+
};
138+
36139
export const getMetadataValueFrom = <
37140
T extends AllowedMetadataValueTypes = AllowedMetadataValueTypes,
38141
>(
39142
{ meta }: IMetadataHolder,
40143
key: AppMetadataKeys,
41144
defaultValue?: T,
145+
applyMigrations: boolean = true,
42146
): T | undefined => {
43-
const metadataKey = getMetadataKey(key);
147+
const metadata = applyMigrations ? applyMetadataMigrations(meta) : meta;
44148

45-
const isMissingKey = !Object.prototype.hasOwnProperty.call(meta ?? {}, metadataKey);
46-
if (meta === undefined || isMissingKey) {
149+
if (metadata === undefined || !doesMetadataKeyExistIn(metadata, key)) {
47150
return defaultValue;
48151
}
49152

50-
return convertValueFromMetadata(meta[metadataKey]);
153+
return convertValueFromMetadata(metadata[getMetadataKey(key)]);
51154
};
52155

53156
export const getMetadataFrom = (
54157
{ meta }: IMetadataHolder,
55158
keysToDefaultValues: MetadataKeyValuePair[],
159+
applyMigrations?: boolean,
56160
): IMetadata<AllowedMetadataValueTypes> => {
57161
const appMetadata: IMetadata<AllowedMetadataValueTypes> = {};
58162

59163
keysToDefaultValues.forEach(([key, defaultValue]) => {
60-
appMetadata[key] = getMetadataValueFrom({ meta }, key, defaultValue);
164+
appMetadata[key] = getMetadataValueFrom({ meta }, key, defaultValue, applyMigrations);
61165
});
62166

63167
return appMetadata;

src/util/readerSettings.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -13,31 +13,35 @@ export const getDefaultSettings = (forceUndefined: boolean = false) => ({
1313
staticNav: forceUndefined ? undefined : false,
1414
showPageNumber: forceUndefined ? undefined : true,
1515
continuesPageGap: forceUndefined ? undefined : false,
16-
loadNextonEnding: forceUndefined ? undefined : false,
16+
loadNextOnEnding: forceUndefined ? undefined : false,
1717
readerType: forceUndefined ? undefined : 'ContinuesVertical',
1818
} as IReaderSettings);
1919

2020
const getReaderSettingsWithDefaultValueFallback = (
2121
meta?: IMetadata,
2222
defaultSettings?: IReaderSettings,
23+
applyMetadataMigration: boolean = true,
2324
): IReaderSettings => ({
2425
...getMetadataFrom(
2526
{ meta },
2627
Object.entries(defaultSettings ?? getDefaultSettings()) as MetadataKeyValuePair[],
28+
applyMetadataMigration,
2729
) as unknown as IReaderSettings,
2830
});
2931

3032
export const getReaderSettingsFromMetadata = (
3133
meta?: IMetadata,
3234
defaultSettings?: IReaderSettings,
35+
applyMetadataMigration?: boolean,
3336
): IReaderSettings => ({
34-
...getReaderSettingsWithDefaultValueFallback(meta, defaultSettings),
37+
...getReaderSettingsWithDefaultValueFallback(meta, defaultSettings, applyMetadataMigration),
3538
});
3639

3740
export const getReaderSettingsFor = (
3841
{ meta }: IMetadataHolder,
3942
defaultSettings?: IReaderSettings,
40-
): IReaderSettings => getReaderSettingsFromMetadata(meta, defaultSettings);
43+
applyMetadataMigration?: boolean,
44+
): IReaderSettings => getReaderSettingsFromMetadata(meta, defaultSettings, applyMetadataMigration);
4145

4246
export const useDefaultReaderSettings = (): {
4347
metadata?: IMetadata,
@@ -63,7 +67,7 @@ export const checkAndHandleMissingStoredReaderSettings = async (
6367
defaultSettings: IReaderSettings,
6468
): Promise<void | void[]> => {
6569
const meta = metadataHolder.meta ?? metadataHolder as IMetadata;
66-
const settingsToCheck = getReaderSettingsFor({ meta }, getDefaultSettings(true));
70+
const settingsToCheck = getReaderSettingsFor({ meta }, getDefaultSettings(true), false);
6771
const newSettings = getReaderSettingsFor({ meta }, defaultSettings);
6872

6973
const undefinedSettings = Object.entries(settingsToCheck)

0 commit comments

Comments
 (0)