2023-01-06 17:15:29 +01:00
|
|
|
/*
|
|
|
|
|
* 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/.
|
|
|
|
|
*/
|
|
|
|
|
|
2023-02-24 16:40:37 +01:00
|
|
|
import {
|
|
|
|
|
AllowedMetadataValueTypes,
|
|
|
|
|
AppMetadataKeys,
|
2023-09-08 00:44:01 +02:00
|
|
|
GqlMetaHolder,
|
2023-06-04 00:50:58 +02:00
|
|
|
IMetadataMigration,
|
2023-04-22 11:34:19 +02:00
|
|
|
Metadata,
|
|
|
|
|
MetadataHolder,
|
2023-02-24 16:40:37 +01:00
|
|
|
MetadataKeyValuePair,
|
2023-06-05 01:42:39 +02:00
|
|
|
} from '@/typings';
|
2023-09-08 00:44:01 +02:00
|
|
|
import requestManager from '@/lib/requests/RequestManager.ts';
|
|
|
|
|
import { CategoryType, ChapterType, MangaType, MetaType } from '@/lib/graphql/generated/graphql.ts';
|
2023-01-06 17:15:29 +01:00
|
|
|
|
|
|
|
|
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-04-22 11:34:19 +02:00
|
|
|
const doesMetadataKeyExistIn = (meta: Metadata | undefined, key: string, appPrefix?: string): boolean =>
|
2023-02-08 18:19:26 +01:00
|
|
|
Object.prototype.hasOwnProperty.call(meta ?? {}, getMetadataKey(key, appPrefix));
|
2023-01-06 17:15:29 +01:00
|
|
|
|
2023-02-06 10:06:33 +01:00
|
|
|
const convertValueFromMetadata = <T extends AllowedMetadataValueTypes = AllowedMetadataValueTypes>(
|
2023-01-06 17:15:29 +01:00
|
|
|
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-09-08 00:44:01 +02:00
|
|
|
export const convertFromGqlMeta = (gqlMetadata?: MetaType[]): Metadata | undefined => {
|
|
|
|
|
if (!gqlMetadata) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const metadata: Metadata = {};
|
|
|
|
|
gqlMetadata.forEach(({ key, value }) => {
|
|
|
|
|
metadata[key] = value;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return metadata;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const convertToGqlMeta = (metadata?: Metadata): MetaType[] | undefined => {
|
|
|
|
|
if (!metadata) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Object.entries(metadata).map(([key, value]) => ({ key, value }));
|
|
|
|
|
};
|
|
|
|
|
|
2023-04-22 11:34:19 +02:00
|
|
|
const getAppMetadataFrom = (meta: Metadata, appPrefix: string = APP_METADATA_KEY_PREFIX): Metadata => {
|
|
|
|
|
const appMetadata: Metadata = {};
|
2023-01-06 18:19:45 +01:00
|
|
|
|
|
|
|
|
Object.entries(meta).forEach(([key, value]) => {
|
|
|
|
|
if (key.startsWith(appPrefix)) {
|
|
|
|
|
appMetadata[key] = value;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return appMetadata;
|
|
|
|
|
};
|
|
|
|
|
|
2023-04-22 11:34:19 +02:00
|
|
|
const applyAppKeyPrefixMigration = (meta: Metadata, migration: IMetadataMigration): Metadata => {
|
|
|
|
|
const migratedMetadata: Metadata = { ...meta };
|
2023-01-06 18:19:45 +01:00
|
|
|
|
|
|
|
|
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-04-22 11:34:19 +02:00
|
|
|
const applyMetadataValueMigration = (meta: Metadata, migration: IMetadataMigration, appKeyPrefix: string): Metadata => {
|
|
|
|
|
const migratedMetadata: Metadata = { ...meta };
|
2023-02-17 13:10:44 +01:00
|
|
|
|
|
|
|
|
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-04-22 11:34:19 +02:00
|
|
|
const applyMetadataKeyMigration = (meta: Metadata, migration: IMetadataMigration): Metadata => {
|
|
|
|
|
const migratedMetadata: Metadata = { ...meta };
|
2023-01-06 18:19:45 +01:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
};
|
|
|
|
|
|
2023-04-22 11:34:19 +02:00
|
|
|
const applyMetadataMigrations = (meta?: Metadata): Metadata | undefined => {
|
2023-01-06 18:19:45 +01:00
|
|
|
if (!meta) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-22 11:34:19 +02:00
|
|
|
const migrationToMetadata: [number, Metadata][] = [[0, meta]];
|
2023-01-06 18:19:45 +01:00
|
|
|
|
|
|
|
|
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-04-22 11:34:19 +02:00
|
|
|
export const getMetadataValueFrom = <Key extends AppMetadataKeys, Value extends AllowedMetadataValueTypes>(
|
|
|
|
|
{ meta }: MetadataHolder,
|
|
|
|
|
key: Key,
|
|
|
|
|
defaultValue?: Value,
|
2023-01-06 18:19:45 +01:00
|
|
|
applyMigrations: boolean = true,
|
2023-04-22 11:34:19 +02:00
|
|
|
): Value | undefined => {
|
2023-01-06 18:19:45 +01:00
|
|
|
const metadata = applyMigrations ? applyMetadataMigrations(meta) : meta;
|
2023-01-06 17:15:29 +01:00
|
|
|
|
2023-01-06 18:19:45 +01:00
|
|
|
if (metadata === undefined || !doesMetadataKeyExistIn(metadata, key)) {
|
2023-01-06 17:15:29 +01:00
|
|
|
return defaultValue;
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-06 18:19:45 +01:00
|
|
|
return convertValueFromMetadata(metadata[getMetadataKey(key)]);
|
2023-01-06 17:15:29 +01:00
|
|
|
};
|
|
|
|
|
|
2023-04-22 11:34:19 +02:00
|
|
|
export const getMetadataFrom = <METADATA extends Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>>(
|
|
|
|
|
{ meta }: MetadataHolder,
|
|
|
|
|
metadataWithDefaultValues: METADATA,
|
2023-01-06 18:19:45 +01:00
|
|
|
applyMigrations?: boolean,
|
2023-04-22 11:34:19 +02:00
|
|
|
): METADATA => {
|
|
|
|
|
const appMetadata = {} as METADATA;
|
|
|
|
|
|
|
|
|
|
Object.entries(metadataWithDefaultValues).forEach(([key, defaultValue]) => {
|
|
|
|
|
appMetadata[key as AppMetadataKeys] = getMetadataValueFrom(
|
|
|
|
|
{ meta },
|
|
|
|
|
key as AppMetadataKeys,
|
|
|
|
|
defaultValue,
|
|
|
|
|
applyMigrations,
|
|
|
|
|
);
|
2023-01-06 17:15:29 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return appMetadata;
|
|
|
|
|
};
|
|
|
|
|
|
2023-09-08 00:44:01 +02:00
|
|
|
// @ts-ignore
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
2023-04-22 11:34:19 +02:00
|
|
|
const wrapMetadataWithMetaKey = (wrap: boolean, metadata: Metadata): MetadataHolder => {
|
2023-01-06 17:15:29 +01:00
|
|
|
if (wrap) {
|
|
|
|
|
return {
|
|
|
|
|
meta: {
|
|
|
|
|
...metadata,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...metadata,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2023-06-04 00:50:58 +02:00
|
|
|
type MetadataHolderType = 'manga' | 'chapter' | 'category' | 'global';
|
|
|
|
|
|
2023-01-06 17:15:29 +01:00
|
|
|
export const requestUpdateMetadataValue = async (
|
2023-09-08 00:44:01 +02:00
|
|
|
metadataHolder: GqlMetaHolder,
|
2023-06-04 00:50:58 +02:00
|
|
|
holderType: MetadataHolderType,
|
2023-01-06 17:15:29 +01:00
|
|
|
key: AppMetadataKeys,
|
|
|
|
|
value: AllowedMetadataValueTypes,
|
|
|
|
|
): Promise<void> => {
|
|
|
|
|
const metadataKey = getMetadataKey(key);
|
|
|
|
|
|
2023-06-04 00:50:58 +02:00
|
|
|
switch (holderType) {
|
|
|
|
|
case 'category':
|
2023-09-08 00:44:01 +02:00
|
|
|
await requestManager.setCategoryMeta((metadataHolder as CategoryType).id, metadataKey, value).response;
|
2023-06-04 00:50:58 +02:00
|
|
|
break;
|
|
|
|
|
case 'chapter':
|
2023-09-08 00:44:01 +02:00
|
|
|
await requestManager.setChapterMeta((metadataHolder as ChapterType).id, metadataKey, value).response;
|
2023-06-04 00:50:58 +02:00
|
|
|
break;
|
|
|
|
|
case 'global':
|
|
|
|
|
await requestManager.setGlobalMetadata(metadataKey, value).response;
|
|
|
|
|
break;
|
|
|
|
|
case 'manga':
|
2023-09-08 00:44:01 +02:00
|
|
|
await requestManager.setMangaMeta((metadataHolder as MangaType).id, metadataKey, value).response;
|
2023-06-04 00:50:58 +02:00
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
throw new Error(`requestUpdateMetadataValue: unknown holderType "${holderType}"`);
|
|
|
|
|
}
|
2023-01-06 17:15:29 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const requestUpdateMetadata = async (
|
2023-09-08 00:44:01 +02:00
|
|
|
metadataHolder: GqlMetaHolder,
|
2023-06-04 00:50:58 +02:00
|
|
|
holderType: MetadataHolderType,
|
2023-01-06 17:15:29 +01:00
|
|
|
keysToValues: [AppMetadataKeys, AllowedMetadataValueTypes][],
|
2023-02-06 10:06:33 +01:00
|
|
|
): Promise<void[]> =>
|
2023-06-04 00:50:58 +02:00
|
|
|
Promise.all(keysToValues.map(([key, value]) => requestUpdateMetadataValue(metadataHolder, holderType, key, value)));
|
2023-01-06 17:15:29 +01:00
|
|
|
|
|
|
|
|
export const requestUpdateServerMetadata = async (
|
2023-09-08 00:44:01 +02:00
|
|
|
serverMetadata: MetaType[],
|
2023-01-06 17:15:29 +01:00
|
|
|
keysToValues: MetadataKeyValuePair[],
|
2023-06-04 00:50:58 +02:00
|
|
|
): Promise<void[]> => requestUpdateMetadata({ meta: serverMetadata }, 'global', keysToValues);
|
2023-01-06 17:15:29 +01:00
|
|
|
|
|
|
|
|
export const requestUpdateMangaMetadata = async (
|
2023-09-08 00:44:01 +02:00
|
|
|
manga: MangaType,
|
2023-01-06 17:15:29 +01:00
|
|
|
keysToValues: MetadataKeyValuePair[],
|
2023-06-04 00:50:58 +02:00
|
|
|
): Promise<void[]> => requestUpdateMetadata(manga, 'manga', keysToValues);
|
2023-01-06 17:15:29 +01:00
|
|
|
|
|
|
|
|
export const requestUpdateChapterMetadata = async (
|
2023-09-08 00:44:01 +02:00
|
|
|
chapter: ChapterType,
|
2023-01-06 17:15:29 +01:00
|
|
|
keysToValues: MetadataKeyValuePair[],
|
2023-09-08 00:44:01 +02:00
|
|
|
): Promise<void[]> => requestUpdateMetadata(chapter, 'chapter', keysToValues);
|
2023-01-06 17:15:29 +01:00
|
|
|
|
|
|
|
|
export const requestUpdateCategoryMetadata = async (
|
2023-09-08 00:44:01 +02:00
|
|
|
category: CategoryType,
|
2023-01-06 17:15:29 +01:00
|
|
|
keysToValues: MetadataKeyValuePair[],
|
2023-06-04 00:50:58 +02:00
|
|
|
): Promise<void[]> => requestUpdateMetadata(category, 'category', keysToValues);
|