Skip to content

Commit

Permalink
add logic for metadata migration (#218)
Browse files Browse the repository at this point in the history
* [#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
  • Loading branch information
schroda authored Jan 6, 2023
1 parent 7642b58 commit 9702728
Show file tree
Hide file tree
Showing 10 changed files with 135 additions and 22 deletions.
2 changes: 1 addition & 1 deletion src/components/navbar/ReaderNavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ export default function ReaderNavBar(props: IProps) {
setSettingValue={updateSettingValue}
staticNav={settings.staticNav}
showPageNumber={settings.showPageNumber}
loadNextonEnding={settings.loadNextonEnding}
loadNextOnEnding={settings.loadNextOnEnding}
readerType={settings.readerType}
/>
</Collapse>
Expand Down
6 changes: 3 additions & 3 deletions src/components/reader/ReaderSettingsOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ interface IProps extends IReaderSettings {
}

export default function ReaderSettingsOptions({
staticNav, loadNextonEnding, readerType, showPageNumber, setSettingValue,
staticNav, loadNextOnEnding, readerType, showPageNumber, setSettingValue,
}: IProps) {
return (
<>
Expand Down Expand Up @@ -49,8 +49,8 @@ export default function ReaderSettingsOptions({
<ListItemSecondaryAction>
<Switch
edge="end"
checked={loadNextonEnding}
onChange={(e) => setSettingValue('loadNextonEnding', e.target.checked)}
checked={loadNextOnEnding}
onChange={(e) => setSettingValue('loadNextOnEnding', e.target.checked)}
/>
</ListItemSecondaryAction>
</ListItem>
Expand Down
2 changes: 1 addition & 1 deletion src/components/reader/pager/DoublePagedPager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export default function DoublePagedPager(props: IReaderProps) {
if (curPage < pages.length - 1) {
const nextCurPage = curPage + pagesDisplayed.current;
setCurPage((nextCurPage >= pages.length) ? pages.length - 1 : nextCurPage);
} else if (settings.loadNextonEnding) {
} else if (settings.loadNextOnEnding) {
nextChapter();
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/reader/pager/HorizontalPager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export default function HorizontalPager(props: IReaderProps) {
if (curPage < pages.length - 1) {
pagesRef.current[curPage + 1]?.scrollIntoView({ inline: 'center' });
setCurPage((page) => page + 1);
} else if (settings.loadNextonEnding) {
} else if (settings.loadNextOnEnding) {
nextChapter();
}
}
Expand Down Expand Up @@ -126,7 +126,7 @@ export default function HorizontalPager(props: IReaderProps) {
}, [selfRef]);

useEffect(() => {
if (settings.loadNextonEnding) {
if (settings.loadNextOnEnding) {
document.addEventListener('scroll', handleLoadNextonEnding);
}
selfRef.current?.addEventListener('mousedown', clickControl);
Expand Down
2 changes: 1 addition & 1 deletion src/components/reader/pager/PagedPager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default function PagedReader(props: IReaderProps) {
function nextPage() {
if (curPage < pages.length - 1) {
changePage(curPage + 1);
} else if (settings.loadNextonEnding) {
} else if (settings.loadNextOnEnding) {
nextChapter();
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/reader/pager/VerticalPager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export default function VerticalPager(props: IReaderProps) {
setCurPage(currentPageRef.current);

// Go to next chapter if configured to and at bottom
if (settings.loadNextonEnding) {
if (settings.loadNextOnEnding) {
nextChapter();
}
} else {
Expand All @@ -72,7 +72,7 @@ export default function VerticalPager(props: IReaderProps) {
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, [settings.loadNextonEnding]);
}, [settings.loadNextOnEnding]);

const go = useCallback((direction: 'up' | 'down') => {
if (direction === 'down' && isAtBottom()) {
Expand Down
2 changes: 1 addition & 1 deletion src/screens/settings/DefaultReaderSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export default function DefaultReaderSettings() {
setSettingValue={setSettingValue}
staticNav={settings.staticNav}
showPageNumber={settings.showPageNumber}
loadNextonEnding={settings.loadNextonEnding}
loadNextOnEnding={settings.loadNextOnEnding}
readerType={settings.readerType}
/>
);
Expand Down
7 changes: 6 additions & 1 deletion src/typings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ interface IState {
index: number
}

interface IMetadataMigration {
appKeyPrefix?: { oldPrefix: string, newPrefix: string }
keys?: { oldKey: string, newKey: string }[]
}

interface IMetadata<VALUES extends AllowedMetadataValueTypes = string> {
[key: string]: VALUES;
}
Expand Down Expand Up @@ -168,7 +173,7 @@ type ReaderType =
interface IReaderSettings{
staticNav: boolean
showPageNumber: boolean
loadNextonEnding: boolean
loadNextOnEnding: boolean
readerType: ReaderType
}

Expand Down
116 changes: 110 additions & 6 deletions src/util/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,21 @@ import client from './client';

const APP_METADATA_KEY_PREFIX = 'webUI_';

const getMetadataKey = (key: string) => `${APP_METADATA_KEY_PREFIX}${key}`;
const migrations: IMetadataMigration[] = [
{
keys: [
{ oldKey: 'loadNextonEnding', newKey: 'loadNextOnEnding' },
],
},
];

const getMetadataKey = (key: string, appPrefix: string = APP_METADATA_KEY_PREFIX) => `${appPrefix}${key}`;

const doesMetadataKeyExistIn = (
meta: IMetadata | undefined,
key: string,
appPrefix?: string,
): boolean => Object.prototype.hasOwnProperty.call(meta ?? {}, getMetadataKey(key, appPrefix));

const convertValueFromMetadata = <
T extends AllowedMetadataValueTypes = AllowedMetadataValueTypes,
Expand All @@ -33,31 +47,121 @@ const convertValueFromMetadata = <
return value as T;
};

const getAppMetadataFrom = (
meta: IMetadata,
appPrefix: string = APP_METADATA_KEY_PREFIX,
): IMetadata => {
const appMetadata: IMetadata = {};

Object.entries(meta).forEach(([key, value]) => {
if (key.startsWith(appPrefix)) {
appMetadata[key] = value;
}
});

return appMetadata;
};

const applyAppKeyPrefixMigration = (meta: IMetadata, migration: IMetadataMigration): IMetadata => {
const migratedMetadata: IMetadata = { ...meta };

if (!migration.appKeyPrefix) {
return migratedMetadata;
}

const { oldPrefix, newPrefix } = migration.appKeyPrefix;

const oldAppMetadata = getAppMetadataFrom(meta, oldPrefix);
const newAppMetadata = getAppMetadataFrom(meta, newPrefix);

const missingMetadataKeys = Object.keys(oldAppMetadata)
.filter((key) => !Object.keys(newAppMetadata).includes(key));

const isMissingOldMetadata = missingMetadataKeys.length;
if (isMissingOldMetadata) {
missingMetadataKeys.forEach((oldKey) => {
const keyWithNewPrefix = oldKey.replace(oldPrefix, newPrefix);
migratedMetadata[keyWithNewPrefix] = oldAppMetadata[oldKey];
});
}

return migratedMetadata;
};

const applyMetadataKeyMigration = (meta: IMetadata, migration: IMetadataMigration): IMetadata => {
const migratedMetadata: IMetadata = { ...meta };

if (!migration.keys) {
return migratedMetadata;
}

const metadataKeyChanges = migration.keys;

metadataKeyChanges.forEach(({ oldKey, newKey }) => {
if (!doesMetadataKeyExistIn(meta, oldKey)) {
return;
}

if (doesMetadataKeyExistIn(meta, newKey)) {
return;
}

migratedMetadata[getMetadataKey(newKey)] = meta[getMetadataKey(oldKey)];
});

return migratedMetadata;
};

const applyMetadataMigrations = (meta?: IMetadata): IMetadata | undefined => {
if (!meta) {
return undefined;
}

const migrationToMetadata: [number, IMetadata][] = [[0, meta]];

migrations.forEach((migration, index) => {
const migrationId = index + 1;
const metadataToMigrate = migrationToMetadata[migrationId - 1][1];
const appKeyPrefixMigrated = applyAppKeyPrefixMigration(metadataToMigrate, migration);
const metadataKeysMigrated = applyMetadataKeyMigration(appKeyPrefixMigrated, migration);

migrationToMetadata.push([migrationId, metadataKeysMigrated]);
});

const appliedMigration = migrationToMetadata.length > 1;
if (!appliedMigration) {
return { ...meta };
}

return migrationToMetadata.pop()![1];
};

export const getMetadataValueFrom = <
T extends AllowedMetadataValueTypes = AllowedMetadataValueTypes,
>(
{ meta }: IMetadataHolder,
key: AppMetadataKeys,
defaultValue?: T,
applyMigrations: boolean = true,
): T | undefined => {
const metadataKey = getMetadataKey(key);
const metadata = applyMigrations ? applyMetadataMigrations(meta) : meta;

const isMissingKey = !Object.prototype.hasOwnProperty.call(meta ?? {}, metadataKey);
if (meta === undefined || isMissingKey) {
if (metadata === undefined || !doesMetadataKeyExistIn(metadata, key)) {
return defaultValue;
}

return convertValueFromMetadata(meta[metadataKey]);
return convertValueFromMetadata(metadata[getMetadataKey(key)]);
};

export const getMetadataFrom = (
{ meta }: IMetadataHolder,
keysToDefaultValues: MetadataKeyValuePair[],
applyMigrations?: boolean,
): IMetadata<AllowedMetadataValueTypes> => {
const appMetadata: IMetadata<AllowedMetadataValueTypes> = {};

keysToDefaultValues.forEach(([key, defaultValue]) => {
appMetadata[key] = getMetadataValueFrom({ meta }, key, defaultValue);
appMetadata[key] = getMetadataValueFrom({ meta }, key, defaultValue, applyMigrations);
});

return appMetadata;
Expand Down
12 changes: 8 additions & 4 deletions src/util/readerSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,35 @@ export const getDefaultSettings = (forceUndefined: boolean = false) => ({
staticNav: forceUndefined ? undefined : false,
showPageNumber: forceUndefined ? undefined : true,
continuesPageGap: forceUndefined ? undefined : false,
loadNextonEnding: forceUndefined ? undefined : false,
loadNextOnEnding: forceUndefined ? undefined : false,
readerType: forceUndefined ? undefined : 'ContinuesVertical',
} as IReaderSettings);

const getReaderSettingsWithDefaultValueFallback = (
meta?: IMetadata,
defaultSettings?: IReaderSettings,
applyMetadataMigration: boolean = true,
): IReaderSettings => ({
...getMetadataFrom(
{ meta },
Object.entries(defaultSettings ?? getDefaultSettings()) as MetadataKeyValuePair[],
applyMetadataMigration,
) as unknown as IReaderSettings,
});

export const getReaderSettingsFromMetadata = (
meta?: IMetadata,
defaultSettings?: IReaderSettings,
applyMetadataMigration?: boolean,
): IReaderSettings => ({
...getReaderSettingsWithDefaultValueFallback(meta, defaultSettings),
...getReaderSettingsWithDefaultValueFallback(meta, defaultSettings, applyMetadataMigration),
});

export const getReaderSettingsFor = (
{ meta }: IMetadataHolder,
defaultSettings?: IReaderSettings,
): IReaderSettings => getReaderSettingsFromMetadata(meta, defaultSettings);
applyMetadataMigration?: boolean,
): IReaderSettings => getReaderSettingsFromMetadata(meta, defaultSettings, applyMetadataMigration);

export const useDefaultReaderSettings = (): {
metadata?: IMetadata,
Expand All @@ -63,7 +67,7 @@ export const checkAndHandleMissingStoredReaderSettings = async (
defaultSettings: IReaderSettings,
): Promise<void | void[]> => {
const meta = metadataHolder.meta ?? metadataHolder as IMetadata;
const settingsToCheck = getReaderSettingsFor({ meta }, getDefaultSettings(true));
const settingsToCheck = getReaderSettingsFor({ meta }, getDefaultSettings(true), false);
const newSettings = getReaderSettingsFor({ meta }, defaultSettings);

const undefinedSettings = Object.entries(settingsToCheck)
Expand Down

0 comments on commit 9702728

Please sign in to comment.