Commit new metadata after migration

This commit is contained in:
schroda
2025-01-05 02:15:03 +01:00
parent 040584989e
commit d657ad3999
3 changed files with 295 additions and 166 deletions

View File

@@ -6,9 +6,27 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { APP_METADATA_KEY_PREFIX, METADATA_MIGRATIONS } from '@/modules/metadata/Metadata.constants.ts';
import { IMetadataMigration, Metadata } from '@/modules/metadata/Metadata.types.ts';
import { doesMetadataKeyExistIn, getMetadataKey } from '@/modules/metadata/Metadata.utils.ts';
import { useEffect } from 'react';
import {
APP_METADATA_KEY_PREFIX,
METADATA_MIGRATIONS,
VALID_APP_METADATA_KEYS,
} from '@/modules/metadata/Metadata.constants.ts';
import {
AppMetadataKeys,
IMetadataMigration,
Metadata,
MetadataHolder,
MetadataHolderType,
MetadataKeyValuePair,
} from '@/modules/metadata/Metadata.types.ts';
import { doesMetadataKeyExistIn, extractOriginalKey, getMetadataKey } from '@/modules/metadata/Metadata.utils.ts';
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
import { ChapterIdInfo } from '@/modules/chapter/services/Chapters.ts';
import { CategoryIdInfo } from '@/modules/category/Category.types.ts';
import { SourceType } from '@/lib/graphql/generated/graphql.ts';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { getMetadataDeleteFunction, getMetadataUpdateFunction } from '@/modules/metadata/services/MetadataUpdater.ts';
const getAppKeyPrefixForMigration = (migrationId: number): string => {
const appKeyPrefix = METADATA_MIGRATIONS.slice(0, migrationId)
@@ -118,20 +136,159 @@ const applyMetadataKeyMigration = (meta: Metadata, migration: IMetadataMigration
return migratedMetadata;
};
export const applyMetadataMigrations = (meta?: Metadata): Metadata | undefined => {
if (!meta) {
return undefined;
const getOutdatedMetadataKeys = (metadata?: Metadata): AppMetadataKeys[] => {
if (!metadata) {
return [];
}
const oldAppKeyPrefixes = METADATA_MIGRATIONS.reduce((acc, migration) => {
const oldPrefix = migration.appKeyPrefix?.oldPrefix;
if (!oldPrefix) {
return acc;
}
return [...acc, oldPrefix];
}, [] as string[]);
return Object.keys(metadata).filter((key) => {
const appKeyPrefixOfKey = key.split('_')[0];
const isMetadataKeyOfApp = [...oldAppKeyPrefixes, APP_METADATA_KEY_PREFIX].includes(appKeyPrefixOfKey);
if (!isMetadataKeyOfApp) {
return false;
}
const isOldKeyPrefix = oldAppKeyPrefixes.includes(appKeyPrefixOfKey);
if (isOldKeyPrefix) {
return true;
}
return !VALID_APP_METADATA_KEYS.includes(extractOriginalKey(key));
}) as AppMetadataKeys[];
};
const getNewMetadataKeys = (
metadata: Metadata | undefined,
migratedMetadata: Metadata,
metadataKeyToDelete: string[],
): string[] => {
if (!metadata) {
return [];
}
const newKeys = METADATA_MIGRATIONS.reduce((acc, migration) => {
if (!migration.keys) {
return acc;
}
const keys = migration.keys.map(({ newKey }) => newKey).filter((key) => key !== undefined);
return [...acc, ...keys];
}, [] as string[]);
return Object.keys(migratedMetadata).filter((metadataKey) => {
const key = extractOriginalKey(metadataKey);
const isNewKeyOfAMigration = newKeys.includes(key);
const isOldKeyOfAMigration = metadataKeyToDelete.includes(key);
const isAlreadyCommited = Object.hasOwn(metadata, metadataKey);
return !isAlreadyCommited && isNewKeyOfAMigration && !isOldKeyOfAMigration;
});
};
/**
* Prevent spamming requests due to frequent metadata reads while the migration hasn't been commited to the server yet
*/
const commitedMigrations = new Set<string>();
const commitMigratedMetadata = (
type: MetadataHolderType,
metadataHolder:
| MetadataHolder
| (MangaIdInfo & MetadataHolder)
| (ChapterIdInfo & MetadataHolder)
| (CategoryIdInfo & MetadataHolder)
| (Pick<SourceType, 'id'> & MetadataHolder)
| undefined,
migratedMetadata: Metadata,
useEffectFn: typeof useEffect = (fn: () => void) => fn(),
): void => {
const metadata = metadataHolder?.meta;
const metadataKeysToDelete = getOutdatedMetadataKeys(metadata);
const metadataKeysToCommit = getNewMetadataKeys(metadata, migratedMetadata, metadataKeysToDelete);
const metadataToUpdate = metadataKeysToCommit.map((key) => [key, migratedMetadata[key]]) as MetadataKeyValuePair[];
const updateMetadata = getMetadataUpdateFunction(type, metadataHolder ?? { id: -1, meta: {} });
const deleteMetadata = getMetadataDeleteFunction(type, metadataHolder ?? { id: -1, meta: {} });
useEffectFn(() => {
(async () => {
const itemMigrationKey = `${type}_${type === 'global' ? '' : (metadataHolder as { id: any }).id}`;
const commitMigration = !commitedMigrations.has(itemMigrationKey);
if (!commitMigration) {
return;
}
const isMetadataAlreadyMigrated =
!metadata || Number(metadata[getMetadataKey('migration')]) === METADATA_MIGRATIONS.length;
const isCommitRequired =
!isMetadataAlreadyMigrated || !!metadataKeysToDelete.length || !!metadataToUpdate.length;
if (!isCommitRequired) {
return;
}
commitedMigrations.add(itemMigrationKey);
try {
await deleteMetadata(metadataKeysToDelete, undefined, true);
await updateMetadata(
[...metadataToUpdate, [getMetadataKey('migration') as AppMetadataKeys, METADATA_MIGRATIONS.length]],
undefined,
true,
);
} catch (e) {
commitedMigrations.delete(itemMigrationKey);
defaultPromiseErrorHandler(
`MetadataMigrations#commitMigrateMetadata(${type}, ${(metadataHolder as { id: any })?.id})`,
);
}
})();
});
};
export const applyMetadataMigrations = (
type: MetadataHolderType,
metadataHolder?:
| MetadataHolder
| (MangaIdInfo & MetadataHolder)
| (ChapterIdInfo & MetadataHolder)
| (CategoryIdInfo & MetadataHolder)
| (Pick<SourceType, 'id'> & MetadataHolder),
useEffectFn: typeof useEffect = (fn: () => void) => fn(),
): Metadata | undefined => {
const meta = { ...(metadataHolder?.meta ?? {}) };
const migrationIdKey = getMetadataKey('migration');
const appliedMigrationId = Number.isNaN(Number(meta[migrationIdKey]))
? 0
: Math.max(0, Number(meta[migrationIdKey]) - 1);
const migrationToMetadata: [number, Metadata][] = [[appliedMigrationId, meta]];
const migrationToMetadata: [number, Metadata][] = [[0, meta]];
METADATA_MIGRATIONS.forEach((migration, index) => {
const migrationId = index + 1;
const metadataToMigrate = migrationToMetadata[migrationId - 1][1];
const isMigrationRequired = appliedMigrationId < migrationId;
if (!isMigrationRequired) {
migrationToMetadata.push([migrationId, metadataToMigrate]);
return;
}
const appKeyPrefixMigrated = applyAppKeyPrefixMigration(metadataToMigrate, migration);
const metadataValuesMigrated = applyMetadataValueMigration(
appKeyPrefixMigrated,
@@ -143,10 +300,9 @@ export const applyMetadataMigrations = (meta?: Metadata): Metadata | undefined =
migrationToMetadata.push([migrationId, metadataKeysMigrated]);
});
const appliedMigration = migrationToMetadata.length > 1;
if (!appliedMigration) {
return { ...meta };
}
const migratedMetadata = migrationToMetadata.pop()?.[1] ?? meta;
return migrationToMetadata.pop()![1];
commitMigratedMetadata(type, metadataHolder, migratedMetadata, useEffectFn);
return migratedMetadata;
};

View File

@@ -7,34 +7,20 @@
*/
import { useEffect } from 'react';
import { METADATA_MIGRATIONS, VALID_APP_METADATA_KEYS } from '@/modules/metadata/Metadata.constants.ts';
import { convertToGqlMeta, convertValueFromMetadata } from '@/modules/metadata/services/MetadataConverter.ts';
import { convertValueFromMetadata } from '@/modules/metadata/services/MetadataConverter.ts';
import {
AllowedMetadataValueTypes,
AppMetadataKeys,
Metadata,
MetadataHolder,
MetadataHolderType,
MetadataKeyValuePair,
} from '@/modules/metadata/Metadata.types.ts';
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
import {
requestDeleteCategoryMetadata,
requestDeleteChapterMetadata,
requestDeleteMangaMetadata,
requestDeleteServerMetadata,
requestDeleteSourceMetadata,
requestUpdateCategoryMetadata,
requestUpdateChapterMetadata,
requestUpdateMangaMetadata,
requestUpdateServerMetadata,
requestUpdateSourceMetadata,
} from '@/modules/metadata/services/MetadataUpdater.ts';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { ChapterIdInfo } from '@/modules/chapter/services/Chapters.ts';
import { CategoryIdInfo } from '@/modules/category/Category.types.ts';
import { SourceType } from '@/lib/graphql/generated/graphql.ts';
import { doesMetadataKeyExistIn, extractOriginalKey, getMetadataKey } from '@/modules/metadata/Metadata.utils.ts';
import { doesMetadataKeyExistIn, getMetadataKey } from '@/modules/metadata/Metadata.utils.ts';
import { applyMetadataMigrations } from '@/modules/metadata/services/MetadataMigrations.ts';
const getMetadataValueFrom = <Key extends AppMetadataKeys, Value extends AllowedMetadataValueTypes>(
@@ -54,99 +40,6 @@ const getMetadataValueFrom = <Key extends AppMetadataKeys, Value extends Allowed
return convertValueFromMetadata(metadata[getMetadataKey(key, prefixes)]);
};
const getMetadataUpdateFunction = (
type: MetadataHolderType,
metadataHolder:
| MetadataHolder
| (MangaIdInfo & MetadataHolder)
| (ChapterIdInfo & MetadataHolder)
| (CategoryIdInfo & MetadataHolder)
| (Pick<SourceType, 'id'> & MetadataHolder),
): ((appliedMigration: [MetadataKeyValuePair]) => Promise<void[]>) => {
switch (type) {
case 'global':
return (appliedMigration) => requestUpdateServerMetadata(appliedMigration);
case 'manga':
return (appliedMigration) =>
requestUpdateMangaMetadata(
{ id: (metadataHolder as MangaIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) },
appliedMigration,
);
case 'chapter':
return (appliedMigration) =>
requestUpdateChapterMetadata(
{ id: (metadataHolder as ChapterIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) },
appliedMigration,
);
case 'category':
return (appliedMigration) =>
requestUpdateCategoryMetadata(
{ id: (metadataHolder as CategoryIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) },
appliedMigration,
);
case 'source':
return (appliedMigration) =>
requestUpdateSourceMetadata(
{
id: (metadataHolder as Pick<SourceType, 'id'>).id,
meta: convertToGqlMeta(metadataHolder.meta),
},
appliedMigration,
);
default:
throw new Error(`Unexpected "type" (${type})`);
}
};
const getMetadataDeleteFunction = (
type: MetadataHolderType,
metadataHolder:
| MetadataHolder
| (MangaIdInfo & MetadataHolder)
| (ChapterIdInfo & MetadataHolder)
| (CategoryIdInfo & MetadataHolder)
| (Pick<SourceType, 'id'> & MetadataHolder),
): ((metadataToDelete: AppMetadataKeys[]) => Promise<void[]>) => {
switch (type) {
case 'global':
return (appliedMigration) => requestDeleteServerMetadata(appliedMigration);
case 'manga':
return (appliedMigration) =>
requestDeleteMangaMetadata(
{ id: (metadataHolder as MangaIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) },
appliedMigration,
);
case 'chapter':
return (appliedMigration) =>
requestDeleteChapterMetadata(
{ id: (metadataHolder as ChapterIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) },
appliedMigration,
);
case 'category':
return (appliedMigration) =>
requestDeleteCategoryMetadata(
{ id: (metadataHolder as CategoryIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) },
appliedMigration,
);
case 'source':
return (appliedMigration) =>
requestDeleteSourceMetadata(
{
id: (metadataHolder as Pick<SourceType, 'id'>).id,
meta: convertToGqlMeta(metadataHolder.meta),
},
appliedMigration,
);
default:
throw new Error(`Unexpected "type" (${type})`);
}
};
/**
* Prevent spamming requests due to frequent metadata reads while the migration hasn't been commited to the server yet
*/
const commitedMigrations = new Set<string>();
export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>>(
type: 'global',
metadataHolder: MetadataHolder,
@@ -194,11 +87,7 @@ export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKey
prefixes?: string[],
useEffectFn: typeof useEffect = (fn: () => void) => fn(),
): METADATA {
const wasMigrated =
!!metadataHolder?.meta &&
Number(getMetadataValueFrom(metadataHolder.meta, 'migration', 0)) !== METADATA_MIGRATIONS.length;
const migratedMetadata = applyMetadataMigrations(metadataHolder.meta);
const migratedMetadata = applyMetadataMigrations(type, metadataHolder, useEffectFn);
const appMetadata = {} as METADATA;
Object.entries(metadataWithDefaultValues).forEach(([key, defaultValue]) => {
@@ -210,29 +99,5 @@ export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKey
);
});
useEffectFn(() => {
const itemMigrationKey = `${type}_${type === 'global' ? '' : (metadataHolder as { id: any }).id}`;
const commitMigration = !commitedMigrations.has(itemMigrationKey);
if (wasMigrated && commitMigration) {
const metadataToDelete = Object.keys(metadataHolder.meta ?? {})
.map(extractOriginalKey)
.filter((key) => !VALID_APP_METADATA_KEYS.includes(key)) as AppMetadataKeys[];
commitedMigrations.add(itemMigrationKey);
getMetadataDeleteFunction(
type,
metadataHolder,
)(metadataToDelete)
.then(() =>
getMetadataUpdateFunction(type, metadataHolder)([['migration', METADATA_MIGRATIONS.length]]),
)
.catch((error) => {
defaultPromiseErrorHandler(`MetadataReader#getMetadataFromServer`)(error);
commitedMigrations.delete(itemMigrationKey);
});
}
});
return appMetadata;
}

View File

@@ -14,11 +14,13 @@ import {
AllowedMetadataValueTypes,
AppMetadataKeys,
GqlMetaHolder,
MetadataHolder,
MetadataHolderType,
MetadataKeyValuePair,
} from '@/modules/metadata/Metadata.types.ts';
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
import { getMetadataKey } from '@/modules/metadata/Metadata.utils.ts';
import { convertToGqlMeta } from '@/modules/metadata/services/MetadataConverter.ts';
const requestUpdateMetadataValue = async (
metadataHolder: GqlMetaHolder,
@@ -26,8 +28,9 @@ const requestUpdateMetadataValue = async (
key: AppMetadataKeys,
value: AllowedMetadataValueTypes,
keyPrefixes?: string[],
isMetadataKey: boolean = false,
): Promise<void> => {
const metadataKey = getMetadataKey(key, keyPrefixes);
const metadataKey = isMetadataKey ? key : getMetadataKey(key, keyPrefixes);
switch (holderType) {
case 'category':
@@ -56,49 +59,100 @@ const requestUpdateMetadata = async (
holderType: MetadataHolderType,
keysToValues: MetadataKeyValuePair[],
keyPrefixes?: string[],
isMetadataKey?: boolean,
): Promise<void[]> =>
Promise.all(
keysToValues.map(([key, value]) =>
requestUpdateMetadataValue(metadataHolder, holderType, key, value, keyPrefixes),
requestUpdateMetadataValue(metadataHolder, holderType, key, value, keyPrefixes, isMetadataKey),
),
);
export const requestUpdateServerMetadata = async (
keysToValues: MetadataKeyValuePair[],
keyPrefixes?: string[],
): Promise<void[]> => requestUpdateMetadata({}, 'global', keysToValues, keyPrefixes);
isMetadataKey?: boolean,
): Promise<void[]> => requestUpdateMetadata({}, 'global', keysToValues, keyPrefixes, isMetadataKey);
export const requestUpdateMangaMetadata = async (
manga: MangaIdInfo & GqlMetaHolder,
keysToValues: MetadataKeyValuePair[],
keyPrefixes?: string[],
): Promise<void[]> => requestUpdateMetadata(manga, 'manga', keysToValues, keyPrefixes);
isMetadataKey?: boolean,
): Promise<void[]> => requestUpdateMetadata(manga, 'manga', keysToValues, keyPrefixes, isMetadataKey);
export const requestUpdateChapterMetadata = async (
chapter: ChapterIdInfo & GqlMetaHolder,
keysToValues: MetadataKeyValuePair[],
keyPrefixes?: string[],
): Promise<void[]> => requestUpdateMetadata(chapter, 'chapter', keysToValues, keyPrefixes);
isMetadataKey?: boolean,
): Promise<void[]> => requestUpdateMetadata(chapter, 'chapter', keysToValues, keyPrefixes, isMetadataKey);
export const requestUpdateCategoryMetadata = async (
category: CategoryIdInfo & GqlMetaHolder,
keysToValues: MetadataKeyValuePair[],
keyPrefixes?: string[],
): Promise<void[]> => requestUpdateMetadata(category, 'category', keysToValues, keyPrefixes);
isMetadataKey?: boolean,
): Promise<void[]> => requestUpdateMetadata(category, 'category', keysToValues, keyPrefixes, isMetadataKey);
export const requestUpdateSourceMetadata = async (
source: Pick<SourceType, 'id'> & GqlMetaHolder,
keysToValue: MetadataKeyValuePair[],
keyPrefixes?: string[],
): Promise<void[]> => requestUpdateMetadata(source, 'source', keysToValue, keyPrefixes);
isMetadataKey?: boolean,
): Promise<void[]> => requestUpdateMetadata(source, 'source', keysToValue, keyPrefixes, isMetadataKey);
export const getMetadataUpdateFunction = (
type: MetadataHolderType,
metadataHolder:
| MetadataHolder
| (MangaIdInfo & MetadataHolder)
| (ChapterIdInfo & MetadataHolder)
| (CategoryIdInfo & MetadataHolder)
| (Pick<SourceType, 'id'> & MetadataHolder),
): ((keyValuePair: MetadataKeyValuePair[], keyPrefixes?: string[], isMetadataKey?: boolean) => Promise<void[]>) => {
switch (type) {
case 'global':
return (...args) => requestUpdateServerMetadata(...args);
case 'manga':
return (...args) =>
requestUpdateMangaMetadata(
{ id: (metadataHolder as MangaIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) },
...args,
);
case 'chapter':
return (...args) =>
requestUpdateChapterMetadata(
{ id: (metadataHolder as ChapterIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) },
...args,
);
case 'category':
return (...args) =>
requestUpdateCategoryMetadata(
{ id: (metadataHolder as CategoryIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) },
...args,
);
case 'source':
return (...args) =>
requestUpdateSourceMetadata(
{
id: (metadataHolder as Pick<SourceType, 'id'>).id,
meta: convertToGqlMeta(metadataHolder.meta),
},
...args,
);
default:
throw new Error(`Unexpected "type" (${type})`);
}
};
export const requestDeleteMetadataValue = async (
metadataHolder: GqlMetaHolder,
holderType: MetadataHolderType,
key: AppMetadataKeys,
keyPrefixes?: string[],
isMetadataKey: boolean = false,
): Promise<void> => {
const metadataKey = getMetadataKey(key, keyPrefixes);
const metadataKey = isMetadataKey ? key : getMetadataKey(key, keyPrefixes);
switch (holderType) {
case 'category':
@@ -126,33 +180,87 @@ async function requestDeleteMetadata(
holderType: MetadataHolderType,
keys: AppMetadataKeys[],
keyPrefixes?: string[],
isMetadataKey?: boolean,
): Promise<void[]> {
return Promise.all(keys.map((key) => requestDeleteMetadataValue(metadataHolder, holderType, key, keyPrefixes)));
return Promise.all(
keys.map((key) => requestDeleteMetadataValue(metadataHolder, holderType, key, keyPrefixes, isMetadataKey)),
);
}
export const requestDeleteServerMetadata = async (keys: AppMetadataKeys[], keyPrefixes?: string[]): Promise<void[]> =>
requestDeleteMetadata({}, 'global', keys, keyPrefixes);
export const requestDeleteServerMetadata = async (
keys: AppMetadataKeys[],
keyPrefixes?: string[],
isMetadataKey?: boolean,
): Promise<void[]> => requestDeleteMetadata({}, 'global', keys, keyPrefixes, isMetadataKey);
export const requestDeleteMangaMetadata = async (
manga: MangaIdInfo & GqlMetaHolder,
keys: AppMetadataKeys[],
keyPrefixes?: string[],
): Promise<void[]> => requestDeleteMetadata(manga, 'manga', keys, keyPrefixes);
isMetadataKey?: boolean,
): Promise<void[]> => requestDeleteMetadata(manga, 'manga', keys, keyPrefixes, isMetadataKey);
export const requestDeleteChapterMetadata = async (
chapter: ChapterIdInfo & GqlMetaHolder,
keys: AppMetadataKeys[],
keyPrefixes?: string[],
): Promise<void[]> => requestDeleteMetadata(chapter, 'chapter', keys, keyPrefixes);
isMetadataKey?: boolean,
): Promise<void[]> => requestDeleteMetadata(chapter, 'chapter', keys, keyPrefixes, isMetadataKey);
export const requestDeleteCategoryMetadata = async (
category: CategoryIdInfo & GqlMetaHolder,
keys: AppMetadataKeys[],
keyPrefixes?: string[],
): Promise<void[]> => requestDeleteMetadata(category, 'category', keys, keyPrefixes);
isMetadataKey?: boolean,
): Promise<void[]> => requestDeleteMetadata(category, 'category', keys, keyPrefixes, isMetadataKey);
export const requestDeleteSourceMetadata = async (
source: Pick<SourceType, 'id'> & GqlMetaHolder,
keys: AppMetadataKeys[],
keyPrefixes?: string[],
): Promise<void[]> => requestDeleteMetadata(source, 'source', keys, keyPrefixes);
isMetadataKey?: boolean,
): Promise<void[]> => requestDeleteMetadata(source, 'source', keys, keyPrefixes, isMetadataKey);
export const getMetadataDeleteFunction = (
type: MetadataHolderType,
metadataHolder:
| MetadataHolder
| (MangaIdInfo & MetadataHolder)
| (ChapterIdInfo & MetadataHolder)
| (CategoryIdInfo & MetadataHolder)
| (Pick<SourceType, 'id'> & MetadataHolder),
): ((metadataToDelete: AppMetadataKeys[], keyPrefixes?: string[], isMetadataKey?: boolean) => Promise<void[]>) => {
switch (type) {
case 'global':
return (...args) => requestDeleteServerMetadata(...args);
case 'manga':
return (...args) =>
requestDeleteMangaMetadata(
{ id: (metadataHolder as MangaIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) },
...args,
);
case 'chapter':
return (...args) =>
requestDeleteChapterMetadata(
{ id: (metadataHolder as ChapterIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) },
...args,
);
case 'category':
return (...args) =>
requestDeleteCategoryMetadata(
{ id: (metadataHolder as CategoryIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) },
...args,
);
case 'source':
return (...args) =>
requestDeleteSourceMetadata(
{
id: (metadataHolder as Pick<SourceType, 'id'>).id,
meta: convertToGqlMeta(metadataHolder.meta),
},
...args,
);
default:
throw new Error(`Unexpected "type" (${type})`);
}
};