From d657ad399963c8b94e07bc3835a1a086f8a751f3 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Sun, 5 Jan 2025 02:15:03 +0100 Subject: [PATCH] Commit new metadata after migration --- .../metadata/services/MetadataMigrations.ts | 180 ++++++++++++++++-- .../metadata/services/MetadataReader.ts | 143 +------------- .../metadata/services/MetadataUpdater.ts | 138 ++++++++++++-- 3 files changed, 295 insertions(+), 166 deletions(-) diff --git a/src/modules/metadata/services/MetadataMigrations.ts b/src/modules/metadata/services/MetadataMigrations.ts index 5be1f650..91124b39 100644 --- a/src/modules/metadata/services/MetadataMigrations.ts +++ b/src/modules/metadata/services/MetadataMigrations.ts @@ -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(); +const commitMigratedMetadata = ( + type: MetadataHolderType, + metadataHolder: + | MetadataHolder + | (MangaIdInfo & MetadataHolder) + | (ChapterIdInfo & MetadataHolder) + | (CategoryIdInfo & MetadataHolder) + | (Pick & 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 & 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; }; diff --git a/src/modules/metadata/services/MetadataReader.ts b/src/modules/metadata/services/MetadataReader.ts index 3ea054e5..bdf73dab 100644 --- a/src/modules/metadata/services/MetadataReader.ts +++ b/src/modules/metadata/services/MetadataReader.ts @@ -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 = ( @@ -54,99 +40,6 @@ const getMetadataValueFrom = & MetadataHolder), -): ((appliedMigration: [MetadataKeyValuePair]) => Promise) => { - 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).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 & MetadataHolder), -): ((metadataToDelete: AppMetadataKeys[]) => Promise) => { - 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).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(); - export function getMetadataFrom>>( type: 'global', metadataHolder: MetadataHolder, @@ -194,11 +87,7 @@ export function getMetadataFrom 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 { - 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; } diff --git a/src/modules/metadata/services/MetadataUpdater.ts b/src/modules/metadata/services/MetadataUpdater.ts index 8d6df99c..a04754fd 100644 --- a/src/modules/metadata/services/MetadataUpdater.ts +++ b/src/modules/metadata/services/MetadataUpdater.ts @@ -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 => { - 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 => 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 => requestUpdateMetadata({}, 'global', keysToValues, keyPrefixes); + isMetadataKey?: boolean, +): Promise => requestUpdateMetadata({}, 'global', keysToValues, keyPrefixes, isMetadataKey); export const requestUpdateMangaMetadata = async ( manga: MangaIdInfo & GqlMetaHolder, keysToValues: MetadataKeyValuePair[], keyPrefixes?: string[], -): Promise => requestUpdateMetadata(manga, 'manga', keysToValues, keyPrefixes); + isMetadataKey?: boolean, +): Promise => requestUpdateMetadata(manga, 'manga', keysToValues, keyPrefixes, isMetadataKey); export const requestUpdateChapterMetadata = async ( chapter: ChapterIdInfo & GqlMetaHolder, keysToValues: MetadataKeyValuePair[], keyPrefixes?: string[], -): Promise => requestUpdateMetadata(chapter, 'chapter', keysToValues, keyPrefixes); + isMetadataKey?: boolean, +): Promise => requestUpdateMetadata(chapter, 'chapter', keysToValues, keyPrefixes, isMetadataKey); export const requestUpdateCategoryMetadata = async ( category: CategoryIdInfo & GqlMetaHolder, keysToValues: MetadataKeyValuePair[], keyPrefixes?: string[], -): Promise => requestUpdateMetadata(category, 'category', keysToValues, keyPrefixes); + isMetadataKey?: boolean, +): Promise => requestUpdateMetadata(category, 'category', keysToValues, keyPrefixes, isMetadataKey); export const requestUpdateSourceMetadata = async ( source: Pick & GqlMetaHolder, keysToValue: MetadataKeyValuePair[], keyPrefixes?: string[], -): Promise => requestUpdateMetadata(source, 'source', keysToValue, keyPrefixes); + isMetadataKey?: boolean, +): Promise => requestUpdateMetadata(source, 'source', keysToValue, keyPrefixes, isMetadataKey); + +export const getMetadataUpdateFunction = ( + type: MetadataHolderType, + metadataHolder: + | MetadataHolder + | (MangaIdInfo & MetadataHolder) + | (ChapterIdInfo & MetadataHolder) + | (CategoryIdInfo & MetadataHolder) + | (Pick & MetadataHolder), +): ((keyValuePair: MetadataKeyValuePair[], keyPrefixes?: string[], isMetadataKey?: boolean) => Promise) => { + 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).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 => { - 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 { - 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 => - requestDeleteMetadata({}, 'global', keys, keyPrefixes); +export const requestDeleteServerMetadata = async ( + keys: AppMetadataKeys[], + keyPrefixes?: string[], + isMetadataKey?: boolean, +): Promise => requestDeleteMetadata({}, 'global', keys, keyPrefixes, isMetadataKey); export const requestDeleteMangaMetadata = async ( manga: MangaIdInfo & GqlMetaHolder, keys: AppMetadataKeys[], keyPrefixes?: string[], -): Promise => requestDeleteMetadata(manga, 'manga', keys, keyPrefixes); + isMetadataKey?: boolean, +): Promise => requestDeleteMetadata(manga, 'manga', keys, keyPrefixes, isMetadataKey); export const requestDeleteChapterMetadata = async ( chapter: ChapterIdInfo & GqlMetaHolder, keys: AppMetadataKeys[], keyPrefixes?: string[], -): Promise => requestDeleteMetadata(chapter, 'chapter', keys, keyPrefixes); + isMetadataKey?: boolean, +): Promise => requestDeleteMetadata(chapter, 'chapter', keys, keyPrefixes, isMetadataKey); export const requestDeleteCategoryMetadata = async ( category: CategoryIdInfo & GqlMetaHolder, keys: AppMetadataKeys[], keyPrefixes?: string[], -): Promise => requestDeleteMetadata(category, 'category', keys, keyPrefixes); + isMetadataKey?: boolean, +): Promise => requestDeleteMetadata(category, 'category', keys, keyPrefixes, isMetadataKey); export const requestDeleteSourceMetadata = async ( source: Pick & GqlMetaHolder, keys: AppMetadataKeys[], keyPrefixes?: string[], -): Promise => requestDeleteMetadata(source, 'source', keys, keyPrefixes); + isMetadataKey?: boolean, +): Promise => requestDeleteMetadata(source, 'source', keys, keyPrefixes, isMetadataKey); + +export const getMetadataDeleteFunction = ( + type: MetadataHolderType, + metadataHolder: + | MetadataHolder + | (MangaIdInfo & MetadataHolder) + | (ChapterIdInfo & MetadataHolder) + | (CategoryIdInfo & MetadataHolder) + | (Pick & MetadataHolder), +): ((metadataToDelete: AppMetadataKeys[], keyPrefixes?: string[], isMetadataKey?: boolean) => Promise) => { + 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).id, + meta: convertToGqlMeta(metadataHolder.meta), + }, + ...args, + ); + default: + throw new Error(`Unexpected "type" (${type})`); + } +};