Files
suwayomi-material-you-webui/src/util/metadata.ts
T

361 lines
10 KiB
TypeScript
Raw Normal View History

/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { mutate } from 'swr';
2023-02-05 16:15:20 +01:00
import client from 'util/client';
import {
AllowedMetadataValueTypes,
AppMetadataKeys,
ICategory,
IManga,
IMangaCard,
IMangaChapter,
IMetadata,
IMetadataHolder,
IMetadataMigration,
MetadataKeyValuePair,
} from 'typings';
const APP_METADATA_KEY_PREFIX = 'webUI_';
2023-02-17 13:10:44 +01:00
/**
* Once all changes have been done in the current branch, a new migration for all changes should be
* created.
*
* In case a value should be migrated for a specific key, and in the same migration the key is
* getting migrated, the key in the value migration is the "old" key (before the migration to the
* new key).
*
* Migration order (function "applyMetadataMigrations"):
* 1. app metadata key prefix
* 2. app metadata values
* 3. app metadata keys
*
* @example
* // changes:
* // commit: change key "X" to "Z"
* // commit: change key "Y" to "A"
* // commit: fix typo in reader setting "someTipo"
* // commit: add metadata migration
*
* // result:
* // old migrations:
* // const migrations = [
* // {
* // keys: [{ oldKey: 'loadNextonEnding', newKey: 'loadNextOnEnding' }],
* // }
* // ];
*
* // updated migrations
* const migrations = [
* {
* keys: [{ oldKey: 'loadNextonEnding', newKey: 'loadNextOnEnding' }],
* },
* {
* keys: [
* { oldKey: 'X', newKey: 'Z' },
* { oldKey: 'Y', newKey: 'A' },
* ],
* // all stored values in the metadata (of this app) will get migrated
* values: [
* {
* oldValue: 'someTipo',
* newValue: 'someTypo'
* },
* ],
* // to migrate only the value of a specific key (in this case of "someKey"):
* // values: [
* // {
* // key: 'someKey',
* // oldValue: 'someTipo',
* // newValue: 'someTypo',
* // },
* // ],
* },
* ];
*/
2023-01-06 18:19:45 +01:00
const migrations: IMetadataMigration[] = [
{
2023-02-06 10:06:33 +01:00
keys: [{ oldKey: 'loadNextonEnding', newKey: 'loadNextOnEnding' }],
2023-01-06 18:19:45 +01:00
},
];
2023-02-17 13:10:44 +01:00
const getAppKeyPrefixForMigration = (migrationId: number): string => {
const appKeyPrefix = migrations
.slice(0, migrationId)
.reverse()
.find((migration) => !!migration.appKeyPrefix);
return appKeyPrefix?.appKeyPrefix?.newPrefix ?? APP_METADATA_KEY_PREFIX;
};
2023-02-08 18:19:26 +01:00
const getMetadataKey = (key: string, appPrefix: string = APP_METADATA_KEY_PREFIX) => `${appPrefix}${key}`;
2023-01-06 18:19:45 +01:00
2023-02-08 18:19:26 +01:00
const doesMetadataKeyExistIn = (meta: IMetadata | undefined, key: string, appPrefix?: string): boolean =>
Object.prototype.hasOwnProperty.call(meta ?? {}, getMetadataKey(key, appPrefix));
2023-02-06 10:06:33 +01:00
const convertValueFromMetadata = <T extends AllowedMetadataValueTypes = AllowedMetadataValueTypes>(
value: string,
): T => {
if (!Number.isNaN(+value)) {
return +value as T;
}
if (value === 'true' || value === 'false') {
return (value === 'true') as T;
}
if (value === 'undefined') {
return undefined as T;
}
return value as T;
};
2023-02-08 18:19:26 +01:00
const getAppMetadataFrom = (meta: IMetadata, appPrefix: string = APP_METADATA_KEY_PREFIX): IMetadata => {
2023-01-06 18:19:45 +01:00
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);
2023-02-08 18:19:26 +01:00
const missingMetadataKeys = Object.keys(oldAppMetadata).filter((key) => !Object.keys(newAppMetadata).includes(key));
2023-01-06 18:19:45 +01:00
const isMissingOldMetadata = missingMetadataKeys.length;
if (isMissingOldMetadata) {
missingMetadataKeys.forEach((oldKey) => {
const keyWithNewPrefix = oldKey.replace(oldPrefix, newPrefix);
migratedMetadata[keyWithNewPrefix] = oldAppMetadata[oldKey];
});
}
return migratedMetadata;
};
2023-02-17 13:10:44 +01:00
const applyMetadataValueMigration = (
meta: IMetadata,
migration: IMetadataMigration,
appKeyPrefix: string,
): IMetadata => {
const migratedMetadata: IMetadata = { ...meta };
if (!migration.values) {
return migratedMetadata;
}
const appMetadata = getAppMetadataFrom(meta, appKeyPrefix);
const metadataValueChanges = migration.values;
metadataValueChanges.forEach(({ key, oldValue, newValue }) => {
const migrateValue = (metaKey: string) => {
if (meta[metaKey] === oldValue) {
migratedMetadata[metaKey] = newValue;
}
};
const migrateValueOfAllAppKeys = key === undefined;
if (migrateValueOfAllAppKeys) {
Object.keys(appMetadata).forEach((metaKey) => {
migrateValue(metaKey);
});
return;
}
if (!doesMetadataKeyExistIn(meta, key, appKeyPrefix)) {
return;
}
migrateValue(getMetadataKey(key, appKeyPrefix));
});
return migratedMetadata;
};
2023-01-06 18:19:45 +01:00
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);
2023-02-17 13:10:44 +01:00
const metadataValuesMigrated = applyMetadataValueMigration(
appKeyPrefixMigrated,
migration,
getAppKeyPrefixForMigration(migrationId),
);
const metadataKeysMigrated = applyMetadataKeyMigration(metadataValuesMigrated, migration);
2023-01-06 18:19:45 +01:00
migrationToMetadata.push([migrationId, metadataKeysMigrated]);
});
const appliedMigration = migrationToMetadata.length > 1;
if (!appliedMigration) {
return { ...meta };
}
return migrationToMetadata.pop()![1];
};
2023-02-08 18:19:26 +01:00
export const getMetadataValueFrom = <T extends AllowedMetadataValueTypes = AllowedMetadataValueTypes>(
{ meta }: IMetadataHolder,
key: AppMetadataKeys,
defaultValue?: T,
2023-01-06 18:19:45 +01:00
applyMigrations: boolean = true,
): T | undefined => {
2023-01-06 18:19:45 +01:00
const metadata = applyMigrations ? applyMetadataMigrations(meta) : meta;
2023-01-06 18:19:45 +01:00
if (metadata === undefined || !doesMetadataKeyExistIn(metadata, key)) {
return defaultValue;
}
2023-01-06 18:19:45 +01:00
return convertValueFromMetadata(metadata[getMetadataKey(key)]);
};
export const getMetadataFrom = (
{ meta }: IMetadataHolder,
keysToDefaultValues: MetadataKeyValuePair[],
2023-01-06 18:19:45 +01:00
applyMigrations?: boolean,
): IMetadata<AllowedMetadataValueTypes> => {
const appMetadata: IMetadata<AllowedMetadataValueTypes> = {};
keysToDefaultValues.forEach(([key, defaultValue]) => {
2023-01-06 18:19:45 +01:00
appMetadata[key] = getMetadataValueFrom({ meta }, key, defaultValue, applyMigrations);
});
return appMetadata;
};
const wrapMetadataWithMetaKey = (wrap: boolean, metadata: IMetadata): IMetadataHolder => {
if (wrap) {
return {
meta: {
...metadata,
},
};
}
return {
...metadata,
};
};
export const requestUpdateMetadataValue = async (
endpoint: string,
metadataHolder: IMetadataHolder,
key: AppMetadataKeys,
value: AllowedMetadataValueTypes,
endpointToMutate: string = endpoint,
wrapWithMetaKey: boolean = true,
): Promise<void> => {
const restApiVersion = '/api/v1';
const url = `${restApiVersion}${endpoint}/meta`;
const urlToMutate = `${restApiVersion}${endpointToMutate}`;
const metadataKey = getMetadataKey(key);
const valueAsString = `${value}`;
const formData = new FormData();
formData.append('key', metadataKey);
formData.append('value', valueAsString);
const mutatedMetadata = {
...metadataHolder.meta,
[metadataKey]: valueAsString,
};
await client.patch(url, formData);
await mutate(
urlToMutate,
{ ...metadataHolder, ...wrapMetadataWithMetaKey(wrapWithMetaKey, mutatedMetadata) },
{ revalidate: false },
);
};
export const requestUpdateMetadata = async (
endpoint: string,
metadataHolder: IMetadataHolder,
keysToValues: [AppMetadataKeys, AllowedMetadataValueTypes][],
endpointToMutate?: string,
wrapWithMetaKey?: boolean,
2023-02-06 10:06:33 +01:00
): Promise<void[]> =>
Promise.all(
keysToValues.map(([key, value]) =>
2023-02-08 18:19:26 +01:00
requestUpdateMetadataValue(endpoint, metadataHolder, key, value, endpointToMutate, wrapWithMetaKey),
2023-02-06 10:06:33 +01:00
),
);
export const requestUpdateServerMetadata = async (
serverMetadata: IMetadata,
keysToValues: MetadataKeyValuePair[],
2023-02-08 18:19:26 +01:00
): Promise<void[]> => requestUpdateMetadata('', { meta: serverMetadata }, keysToValues, '/meta', false);
export const requestUpdateMangaMetadata = async (
manga: IMangaCard | IManga,
keysToValues: MetadataKeyValuePair[],
): Promise<void[]> => requestUpdateMetadata(`/manga/${manga.id}`, manga, keysToValues);
export const requestUpdateChapterMetadata = async (
mangaChapter: IMangaChapter,
keysToValues: MetadataKeyValuePair[],
2023-02-06 10:06:33 +01:00
): Promise<void[]> =>
requestUpdateMetadata(
`/manga/${mangaChapter.manga.id}/chapter/${mangaChapter.chapter.index}`,
mangaChapter.chapter,
keysToValues,
);
export const requestUpdateCategoryMetadata = async (
category: ICategory,
keysToValues: MetadataKeyValuePair[],
): Promise<void[]> => requestUpdateMetadata(`/category/${category.id}`, category, keysToValues);