From 64df9a9489ed85e1cac9f898eba41b981a332e16 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Sun, 25 Jan 2026 20:32:56 +0100 Subject: [PATCH] Add new single multipurpose metadata update mutation --- .../category/services/CategoryMetadata.ts | 8 +- src/features/manga/services/MangaMetadata.ts | 8 +- .../metadata/services/MetadataMigrations.ts | 12 +- .../metadata/services/MetadataUpdater.ts | 257 +++------ .../reader/settings/ReaderSettingsMetadata.ts | 21 +- .../services/ServerSettingsMetadata.ts | 5 +- .../source/services/SourceMetadata.ts | 8 +- src/lib/graphql/category/CategoryMutation.ts | 29 + src/lib/graphql/chapter/ChapterMutation.ts | 29 + src/lib/graphql/generated/graphql.ts | 60 ++ src/lib/graphql/manga/MangaMutation.ts | 29 + .../metadata/GlobalMetadataMutation.ts | 28 + src/lib/graphql/source/SourceMutation.ts | 29 + src/lib/requests/RequestManager.ts | 520 +++++++++++++++++- 14 files changed, 830 insertions(+), 213 deletions(-) diff --git a/src/features/category/services/CategoryMetadata.ts b/src/features/category/services/CategoryMetadata.ts index 33aabe79..e9396450 100644 --- a/src/features/category/services/CategoryMetadata.ts +++ b/src/features/category/services/CategoryMetadata.ts @@ -7,7 +7,7 @@ */ import { useEffect, useMemo } from 'react'; -import { requestUpdateCategoryMetadata } from '@/features/metadata/services/MetadataUpdater.ts'; +import { requestCategoryMetadataUpdate } from '@/features/metadata/services/MetadataUpdater.ts'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { LibraryOptions } from '@/features/library/Library.types.ts'; import { CategoryIdInfo, CategoryMetadataKeys, ICategoryMetadata } from '@/features/category/Category.types.ts'; @@ -81,9 +81,9 @@ export const updateCategoryMetadata = async < metadataKey: MetadataKey, value: ICategoryMetadata[MetadataKey], ): Promise => - requestUpdateCategoryMetadata(category, [ - [metadataKey, convertAppMetadataToGqlMetadata({ [metadataKey]: value })[metadataKey]], - ]); + requestCategoryMetadataUpdate(category, { + update: [[metadataKey, convertAppMetadataToGqlMetadata({ [metadataKey]: value })[metadataKey]]], + }); export const createUpdateCategoryMetadata = ( diff --git a/src/features/manga/services/MangaMetadata.ts b/src/features/manga/services/MangaMetadata.ts index 43212cf4..0dca9e67 100644 --- a/src/features/manga/services/MangaMetadata.ts +++ b/src/features/manga/services/MangaMetadata.ts @@ -17,7 +17,7 @@ import { MetadataHolder, } from '@/features/metadata/Metadata.types.ts'; import { convertFromGqlMeta } from '@/features/metadata/services/MetadataConverter.ts'; -import { requestUpdateMangaMetadata } from '@/features/metadata/services/MetadataUpdater.ts'; +import { requestMangaMetadataUpdate } from '@/features/metadata/services/MetadataUpdater.ts'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; const DEFAULT_MANGA_METADATA: MangaMetadata = { @@ -69,9 +69,9 @@ export const updateMangaMetadata = async < metadataKey: MetadataKey, value: MangaMetadata[MetadataKey], ): Promise => - requestUpdateMangaMetadata(manga, [ - [metadataKey, convertAppMetadataToGqlMetadata({ [metadataKey]: value })[metadataKey]], - ]); + requestMangaMetadataUpdate(manga, { + update: [[metadataKey, convertAppMetadataToGqlMetadata({ [metadataKey]: value })[metadataKey]]], + }); export const createUpdateMangaMetadata = ( diff --git a/src/features/metadata/services/MetadataMigrations.ts b/src/features/metadata/services/MetadataMigrations.ts index cf452698..938f7ced 100644 --- a/src/features/metadata/services/MetadataMigrations.ts +++ b/src/features/metadata/services/MetadataMigrations.ts @@ -24,7 +24,7 @@ import { extractOriginalKey, getMetadataKey } from '@/features/metadata/Metadata import { MangaIdInfo } from '@/features/manga/Manga.types.ts'; import { CategoryIdInfo } from '@/features/category/Category.types.ts'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; -import { getMetadataDeleteFunction, getMetadataUpdateFunction } from '@/features/metadata/services/MetadataUpdater.ts'; +import { getMetadataUpdateFunction } from '@/features/metadata/services/MetadataUpdater.ts'; import { SourceIdInfo } from '@/features/source/Source.types.ts'; import { ChapterIdInfo } from '@/features/chapter/Chapter.types.ts'; @@ -295,7 +295,6 @@ const commitMigratedMetadata = ( ]) as MetadataKeyValuePair[]; const updateMetadata = getMetadataUpdateFunction(type, metadataHolder ?? { id: -1, meta: {} }); - const deleteMetadata = getMetadataDeleteFunction(type, metadataHolder ?? { id: -1, meta: {} }); useEffectFn(() => { (async () => { @@ -317,9 +316,12 @@ const commitMigratedMetadata = ( commitedMigrations.add(itemMigrationKey); try { - await updateMetadata([...metadataToUpdate], undefined, true); - await deleteMetadata(metadataKeysToDelete, undefined, true); - await updateMetadata([['migration', METADATA_MIGRATIONS.length]]); + await updateMetadata({ + update: metadataToUpdate, + delete: metadataKeysToDelete, + migrate: [['migration', METADATA_MIGRATIONS.length]], + isMetadataKey: true, + }); } catch (e) { commitedMigrations.delete(itemMigrationKey); diff --git a/src/features/metadata/services/MetadataUpdater.ts b/src/features/metadata/services/MetadataUpdater.ts index 8b2515fe..5e5d55d2 100644 --- a/src/features/metadata/services/MetadataUpdater.ts +++ b/src/features/metadata/services/MetadataUpdater.ts @@ -21,82 +21,96 @@ import { convertToGqlMeta } from '@/features/metadata/services/MetadataConverter import { SourceIdInfo } from '@/features/source/Source.types.ts'; import { ChapterIdInfo } from '@/features/chapter/Chapter.types.ts'; -const requestUpdateMetadataValues = async ( +type MetadataUpdateOptions = { + update?: MetadataKeyValuePair[]; + delete?: AppMetadataKeys[]; + /** + * Only ever pass the "migration" key value pair into this. + */ + migrate?: MetadataKeyValuePair[]; + keyPrefixes?: string[]; + /** + * Applies to "update" and "delete" metadata value pairs. "migrate" is excluded from this and is always expected to not be already converted to a metadata key + */ + isMetadataKey?: boolean; +}; + +const requestMetadataUpdate = async ( metadataHolder: GqlMetaHolder, holderType: MetadataHolderType, - keysToValues: MetadataKeyValuePair[], - keyPrefixes?: string[], - isMetadataKey: boolean = false, + { + update: keysToValues = [], + delete: keysToDelete = [], + migrate: keysToMigrate = [], + keyPrefixes, + isMetadataKey = false, + }: MetadataUpdateOptions, ): Promise => { - const metas = keysToValues.map(([key, value]) => ({ + if (keysToMigrate?.length > 1 || (keysToMigrate?.length === 1 && keysToMigrate[0][0] !== 'migration')) { + throw new Error( + `requestMetadataUpdate: "migrate" option must only contain a single key-value pair with the key "migration"`, + ); + } + + const updateMetas = keysToValues.map(([key, value]) => ({ key: isMetadataKey ? key : getMetadataKey(key, keyPrefixes), value: `${value}`, })); + const deleteKeys = keysToDelete.map((key) => (isMetadataKey ? key : getMetadataKey(key, keyPrefixes))); + const migrateMetas = keysToMigrate.map(([key, value]) => ({ + key: getMetadataKey(key, keyPrefixes), + value: `${value}`, + })); + + const updateMetaInput = { + updateInput: { metas: updateMetas }, + deleteInput: { keys: deleteKeys }, + migrateInput: { metas: migrateMetas }, + }; switch (holderType) { case 'category': - await requestManager.setCategoryMeta({ - items: [{ categoryIds: [(metadataHolder as CategoryIdInfo).id], metas }], - }).response; + await requestManager.updateCategoryMeta([(metadataHolder as CategoryIdInfo).id], updateMetaInput).response; break; case 'chapter': - await requestManager.setChapterMeta({ - items: [{ chapterIds: [(metadataHolder as ChapterIdInfo).id], metas }], - }).response; + await requestManager.updateChapterMeta([(metadataHolder as ChapterIdInfo).id], updateMetaInput).response; break; case 'global': - await requestManager.setGlobalMetadata({ - metas, - }).response; + await requestManager.updateGlobalMeta(updateMetaInput).response; break; case 'manga': - await requestManager.setMangaMeta({ - items: [{ mangaIds: [(metadataHolder as MangaIdInfo).id], metas }], - }).response; + await requestManager.updateMangaMeta([(metadataHolder as MangaIdInfo).id], updateMetaInput).response; break; case 'source': - await requestManager.setSourceMeta({ - items: [{ sourceIds: [(metadataHolder as SourceIdInfo).id], metas }], - }).response; + await requestManager.updateSourceMeta([(metadataHolder as SourceIdInfo).id], updateMetaInput).response; break; default: - throw new Error(`requestUpdateMetadataValues: unknown holderType "${holderType}"`); + throw new Error(`requestMetadataUpdate: unknown holderType "${holderType}"`); } }; -export const requestUpdateServerMetadata = async ( - keysToValues: MetadataKeyValuePair[], - keyPrefixes?: string[], - isMetadataKey?: boolean, -): Promise => requestUpdateMetadataValues({}, 'global', keysToValues, keyPrefixes, isMetadataKey); +export const requestServerMetadataUpdate = async (options: MetadataUpdateOptions): Promise => + requestMetadataUpdate({}, 'global', options); -export const requestUpdateMangaMetadata = async ( +export const requestMangaMetadataUpdate = async ( manga: MangaIdInfo & GqlMetaHolder, - keysToValues: MetadataKeyValuePair[], - keyPrefixes?: string[], - isMetadataKey?: boolean, -): Promise => requestUpdateMetadataValues(manga, 'manga', keysToValues, keyPrefixes, isMetadataKey); + options: MetadataUpdateOptions, +): Promise => requestMetadataUpdate(manga, 'manga', options); -export const requestUpdateChapterMetadata = async ( +export const requestChapterMetadataUpdate = async ( chapter: ChapterIdInfo & GqlMetaHolder, - keysToValues: MetadataKeyValuePair[], - keyPrefixes?: string[], - isMetadataKey?: boolean, -): Promise => requestUpdateMetadataValues(chapter, 'chapter', keysToValues, keyPrefixes, isMetadataKey); + options: MetadataUpdateOptions, +): Promise => requestMetadataUpdate(chapter, 'chapter', options); -export const requestUpdateCategoryMetadata = async ( +export const requestCategoryMetadataUpdate = async ( category: CategoryIdInfo & GqlMetaHolder, - keysToValues: MetadataKeyValuePair[], - keyPrefixes?: string[], - isMetadataKey?: boolean, -): Promise => requestUpdateMetadataValues(category, 'category', keysToValues, keyPrefixes, isMetadataKey); + options: MetadataUpdateOptions, +): Promise => requestMetadataUpdate(category, 'category', options); -export const requestUpdateSourceMetadata = async ( +export const requestSourceMetadataUpdate = async ( source: SourceIdInfo & GqlMetaHolder, - keysToValue: MetadataKeyValuePair[], - keyPrefixes?: string[], - isMetadataKey?: boolean, -): Promise => requestUpdateMetadataValues(source, 'source', keysToValue, keyPrefixes, isMetadataKey); + options: MetadataUpdateOptions, +): Promise => requestMetadataUpdate(source, 'source', options); export const getMetadataUpdateFunction = ( type: MetadataHolderType, @@ -106,154 +120,33 @@ export const getMetadataUpdateFunction = ( | (ChapterIdInfo & MetadataHolder) | (CategoryIdInfo & MetadataHolder) | (SourceIdInfo & MetadataHolder), -): ((keyValuePair: MetadataKeyValuePair[], prefixes?: string[], isMetadataKey?: boolean) => Promise) => { +): ((options: MetadataUpdateOptions) => Promise) => { switch (type) { case 'global': - return (...args) => requestUpdateServerMetadata(...args); + return (options) => requestServerMetadataUpdate(options); case 'manga': - return (...args) => - requestUpdateMangaMetadata( + return (options) => + requestMangaMetadataUpdate( { id: (metadataHolder as MangaIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) }, - ...args, + options, ); case 'chapter': - return (...args) => - requestUpdateChapterMetadata( + return (options) => + requestChapterMetadataUpdate( { id: (metadataHolder as ChapterIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) }, - ...args, + options, ); case 'category': - return (...args) => - requestUpdateCategoryMetadata( + return (options) => + requestCategoryMetadataUpdate( { id: (metadataHolder as CategoryIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) }, - ...args, + options, ); case 'source': - return (...args) => - requestUpdateSourceMetadata( - { - id: (metadataHolder as SourceIdInfo).id, - meta: convertToGqlMeta(metadataHolder.meta), - }, - ...args, - ); - default: - throw new Error(`Unexpected "type" (${type})`); - } -}; - -const requestDeleteMetadataValues = async ( - metadataHolder: GqlMetaHolder, - holderType: MetadataHolderType, - keys: AppMetadataKeys[], - keyPrefixes?: string[], - isMetadataKey: boolean = false, -): Promise => { - const metadataKeys = keys.map((key) => (isMetadataKey ? key : getMetadataKey(key, keyPrefixes))); - - switch (holderType) { - case 'category': - await requestManager.deleteCategoryMeta({ - items: [{ categoryIds: [(metadataHolder as CategoryIdInfo).id], keys: metadataKeys }], - }).response; - break; - case 'chapter': - await requestManager.deleteChapterMeta({ - items: [{ chapterIds: [(metadataHolder as ChapterIdInfo).id], keys: metadataKeys }], - }).response; - break; - case 'global': - await requestManager.deleteGlobalMeta({ - keys: metadataKeys, - }).response; - break; - case 'manga': - await requestManager.deleteMangaMeta({ - items: [{ mangaIds: [(metadataHolder as MangaIdInfo).id], keys: metadataKeys }], - }).response; - break; - case 'source': - await requestManager.deleteSourceMeta({ - items: [{ sourceIds: [(metadataHolder as SourceIdInfo).id], keys: metadataKeys }], - }).response; - break; - default: - throw new Error(`requestDeleteMetadataValues: unknown holderType "${holderType}"`); - } -}; - -export const requestDeleteServerMetadata = async ( - keys: AppMetadataKeys[], - keyPrefixes?: string[], - isMetadataKey?: boolean, -): Promise => requestDeleteMetadataValues({}, 'global', keys, keyPrefixes, isMetadataKey); - -export const requestDeleteMangaMetadata = async ( - manga: MangaIdInfo & GqlMetaHolder, - keys: AppMetadataKeys[], - keyPrefixes?: string[], - isMetadataKey?: boolean, -): Promise => requestDeleteMetadataValues(manga, 'manga', keys, keyPrefixes, isMetadataKey); - -export const requestDeleteChapterMetadata = async ( - chapter: ChapterIdInfo & GqlMetaHolder, - keys: AppMetadataKeys[], - keyPrefixes?: string[], - isMetadataKey?: boolean, -): Promise => requestDeleteMetadataValues(chapter, 'chapter', keys, keyPrefixes, isMetadataKey); - -export const requestDeleteCategoryMetadata = async ( - category: CategoryIdInfo & GqlMetaHolder, - keys: AppMetadataKeys[], - keyPrefixes?: string[], - isMetadataKey?: boolean, -): Promise => requestDeleteMetadataValues(category, 'category', keys, keyPrefixes, isMetadataKey); - -export const requestDeleteSourceMetadata = async ( - source: SourceIdInfo & GqlMetaHolder, - keys: AppMetadataKeys[], - keyPrefixes?: string[], - isMetadataKey?: boolean, -): Promise => requestDeleteMetadataValues(source, 'source', keys, keyPrefixes, isMetadataKey); - -export const getMetadataDeleteFunction = ( - type: MetadataHolderType, - metadataHolder: - | MetadataHolder - | (MangaIdInfo & MetadataHolder) - | (ChapterIdInfo & MetadataHolder) - | (CategoryIdInfo & MetadataHolder) - | (SourceIdInfo & 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 SourceIdInfo).id, - meta: convertToGqlMeta(metadataHolder.meta), - }, - ...args, + return (options) => + requestSourceMetadataUpdate( + { id: (metadataHolder as SourceIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) }, + options, ); default: throw new Error(`Unexpected "type" (${type})`); diff --git a/src/features/reader/settings/ReaderSettingsMetadata.ts b/src/features/reader/settings/ReaderSettingsMetadata.ts index 4e431bf2..0fa7ed2f 100644 --- a/src/features/reader/settings/ReaderSettingsMetadata.ts +++ b/src/features/reader/settings/ReaderSettingsMetadata.ts @@ -10,8 +10,8 @@ import { useEffect, useMemo } from 'react'; // eslint-disable-next-line import/no-extraneous-dependencies,no-restricted-imports import { requestManager } from '@/lib/requests/RequestManager.ts'; import { - requestUpdateMangaMetadata, - requestUpdateServerMetadata, + requestMangaMetadataUpdate, + requestServerMetadataUpdate, } from '@/features/metadata/services/MetadataUpdater.ts'; import { MangaType } from '@/lib/graphql/generated/graphql.ts'; import { IReaderSettings, IReaderSettingsWithDefaultFlag, ReadingMode } from '@/features/reader/Reader.types.ts'; @@ -227,17 +227,16 @@ export const updateReaderSettings = async => { const isGlobalSetting = isGlobal || GLOBAL_READER_SETTING_KEYS.includes(setting); if (isGlobalSetting) { - return requestUpdateServerMetadata( - [[setting, convertSettingsToMetadata({ [setting]: value })[setting]]], - profile !== undefined ? [profile?.toString()] : undefined, - ); + return requestServerMetadataUpdate({ + update: [[setting, convertSettingsToMetadata({ [setting]: value })[setting]]], + keyPrefixes: profile !== undefined ? [profile?.toString()] : undefined, + }); } - return requestUpdateMangaMetadata( - manga, - [[setting, convertSettingsToMetadata({ [setting]: value })[setting]]], - profile !== undefined ? [profile?.toString()] : undefined, - ); + return requestMangaMetadataUpdate(manga, { + update: [[setting, convertSettingsToMetadata({ [setting]: value })[setting]]], + keyPrefixes: profile !== undefined ? [profile?.toString()] : undefined, + }); }; export const createUpdateReaderSettings = ( diff --git a/src/features/settings/services/ServerSettingsMetadata.ts b/src/features/settings/services/ServerSettingsMetadata.ts index a39f1622..cc3b305e 100644 --- a/src/features/settings/services/ServerSettingsMetadata.ts +++ b/src/features/settings/services/ServerSettingsMetadata.ts @@ -8,7 +8,7 @@ import { useEffect, useMemo } from 'react'; import { requestManager } from '@/lib/requests/RequestManager.ts'; -import { requestUpdateServerMetadata } from '@/features/metadata/services/MetadataUpdater.ts'; +import { requestServerMetadataUpdate } from '@/features/metadata/services/MetadataUpdater.ts'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { SERVER_SETTINGS_METADATA_DEFAULT } from '@/features/settings/Settings.constants.ts'; import { MetadataServerSettingKeys, MetadataServerSettings } from '@/features/settings/Settings.types.ts'; @@ -64,7 +64,8 @@ export const updateMetadataServerSettings = async < >( setting: Setting, value: MetadataServerSettings[Setting], -): Promise => requestUpdateServerMetadata([[setting, convertSettingsToMetadata({ [setting]: value })[setting]]]); +): Promise => + requestServerMetadataUpdate({ update: [[setting, convertSettingsToMetadata({ [setting]: value })[setting]]] }); export const createUpdateMetadataServerSettings = ( diff --git a/src/features/source/services/SourceMetadata.ts b/src/features/source/services/SourceMetadata.ts index ad8e6f77..f1d83550 100644 --- a/src/features/source/services/SourceMetadata.ts +++ b/src/features/source/services/SourceMetadata.ts @@ -7,7 +7,7 @@ */ import { useEffect, useMemo } from 'react'; -import { requestUpdateSourceMetadata } from '@/features/metadata/services/MetadataUpdater.ts'; +import { requestSourceMetadataUpdate } from '@/features/metadata/services/MetadataUpdater.ts'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { ISourceMetadata, SourceIdInfo, SourceMetadataKeys } from '@/features/source/Source.types.ts'; import { convertFromGqlMeta } from '@/features/metadata/services/MetadataConverter.ts'; @@ -51,9 +51,9 @@ export const updateSourceMetadata = async < metadataKey: MetadataKey, value: ISourceMetadata[MetadataKey], ): Promise => - requestUpdateSourceMetadata(source, [ - [metadataKey, convertAppMetadataToGqlMetadata({ [metadataKey]: value })[metadataKey]], - ]); + requestSourceMetadataUpdate(source, { + update: [[metadataKey, convertAppMetadataToGqlMetadata({ [metadataKey]: value })[metadataKey]]], + }); export const createUpdateSourceMetadata = ( diff --git a/src/lib/graphql/category/CategoryMutation.ts b/src/lib/graphql/category/CategoryMutation.ts index ca44b563..192d6036 100644 --- a/src/lib/graphql/category/CategoryMutation.ts +++ b/src/lib/graphql/category/CategoryMutation.ts @@ -104,3 +104,32 @@ export const UPDATE_CATEGORY_ORDER = gql` } } `; + +export const UPDATE_CATEGORY_METADATA = gql` + ${CATEGORY_META_FIELDS} + + mutation UPDATE_CATEGORY_METADATA( + $updateInput: SetCategoryMetasInput! + $hasUpdates: Boolean! + $deleteInput: DeleteCategoryMetasInput! + $hasDeletions: Boolean! + $migrateInput: SetCategoryMetasInput! + $isMigration: Boolean! + ) { + updatedMeta: setCategoryMetas(input: $updateInput) @include(if: $hasUpdates) { + metas { + ...CATEGORY_META_FIELDS + } + } + deletedMeta: deleteCategoryMetas(input: $deleteInput) @include(if: $hasDeletions) { + metas { + ...CATEGORY_META_FIELDS + } + } + migrationMeta: setCategoryMetas(input: $migrateInput) @include(if: $isMigration) { + metas { + ...CATEGORY_META_FIELDS + } + } + } +`; diff --git a/src/lib/graphql/chapter/ChapterMutation.ts b/src/lib/graphql/chapter/ChapterMutation.ts index 1533b9fd..7c8f5b0f 100644 --- a/src/lib/graphql/chapter/ChapterMutation.ts +++ b/src/lib/graphql/chapter/ChapterMutation.ts @@ -181,3 +181,32 @@ export const UPDATE_CHAPTERS = gql` } } `; + +export const UPDATE_CHAPTER_METADATA = gql` + ${CHAPTER_META_FIELDS} + + mutation UPDATE_CHAPTER_METADATA( + $updateInput: SetChapterMetasInput! + $hasUpdates: Boolean! + $deleteInput: DeleteChapterMetasInput! + $hasDeletions: Boolean! + $migrateInput: SetChapterMetasInput! + $isMigration: Boolean! + ) { + updatedMeta: setChapterMetas(input: $updateInput) @include(if: $hasUpdates) { + metas { + ...CHAPTER_META_FIELDS + } + } + deletedMeta: deleteChapterMetas(input: $deleteInput) @include(if: $hasDeletions) { + metas { + ...CHAPTER_META_FIELDS + } + } + migrationMeta: setChapterMetas(input: $migrateInput) @include(if: $isMigration) { + metas { + ...CHAPTER_META_FIELDS + } + } + } +`; diff --git a/src/lib/graphql/generated/graphql.ts b/src/lib/graphql/generated/graphql.ts index 0d7fac1c..ecbf187f 100644 --- a/src/lib/graphql/generated/graphql.ts +++ b/src/lib/graphql/generated/graphql.ts @@ -3552,6 +3552,18 @@ export type UpdateCategoryOrderMutationVariables = Exact<{ export type UpdateCategoryOrderMutation = { __typename?: 'Mutation', updateCategoryOrder?: { __typename?: 'UpdateCategoryOrderPayload', categories: Array<{ __typename?: 'CategoryType', id: number, order: number }> } | null }; +export type UpdateCategoryMetadataMutationVariables = Exact<{ + updateInput: SetCategoryMetasInput; + hasUpdates: Scalars['Boolean']['input']; + deleteInput: DeleteCategoryMetasInput; + hasDeletions: Scalars['Boolean']['input']; + migrateInput: SetCategoryMetasInput; + isMigration: Scalars['Boolean']['input']; +}>; + + +export type UpdateCategoryMetadataMutation = { __typename?: 'Mutation', updatedMeta?: { __typename?: 'SetCategoryMetasPayload', metas: Array<{ __typename?: 'CategoryMetaType', categoryId: number, key: string, value: string }> } | null, deletedMeta?: { __typename?: 'DeleteCategoryMetasPayload', metas: Array<{ __typename?: 'CategoryMetaType', categoryId: number, key: string, value: string }> } | null, migrationMeta?: { __typename?: 'SetCategoryMetasPayload', metas: Array<{ __typename?: 'CategoryMetaType', categoryId: number, key: string, value: string }> } | null }; + export type GetCategoriesBaseQueryVariables = Exact<{ after?: InputMaybe; before?: InputMaybe; @@ -3671,6 +3683,18 @@ export type UpdateChaptersMutationVariables = Exact<{ export type UpdateChaptersMutation = { __typename?: 'Mutation', updateChapters?: { __typename?: 'UpdateChaptersPayload', chapters: Array<{ __typename?: 'ChapterType', id: number, isBookmarked?: boolean, isRead?: boolean, lastReadAt?: string, lastPageRead?: number, manga?: { __typename?: 'MangaType', id: number, unreadCount: number, bookmarkCount: number, lastReadChapter?: { __typename?: 'ChapterType', id: number } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number } | null, firstUnreadChapter?: { __typename?: 'ChapterType', id: number } | null } }> } | null, deleteDownloadedChapters?: { __typename?: 'DeleteDownloadedChaptersPayload', chapters: Array<{ __typename?: 'ChapterType', id: number, isDownloaded: boolean, manga: { __typename?: 'MangaType', id: number, downloadCount: number } }> } | null, trackProgress?: { __typename?: 'TrackProgressPayload', trackRecords: Array<{ __typename?: 'TrackRecordType', id: number, remoteId: string, trackerId: number, remoteUrl: string, title: string, status: number, lastChapterRead: number, totalChapters: number, score: number, displayScore: string, startDate: string, finishDate: string, private: boolean }> } | null }; +export type UpdateChapterMetadataMutationVariables = Exact<{ + updateInput: SetChapterMetasInput; + hasUpdates: Scalars['Boolean']['input']; + deleteInput: DeleteChapterMetasInput; + hasDeletions: Scalars['Boolean']['input']; + migrateInput: SetChapterMetasInput; + isMigration: Scalars['Boolean']['input']; +}>; + + +export type UpdateChapterMetadataMutation = { __typename?: 'Mutation', updatedMeta?: { __typename?: 'SetChapterMetasPayload', metas: Array<{ __typename?: 'ChapterMetaType', chapterId: number, key: string, value: string }> } | null, deletedMeta?: { __typename?: 'DeleteChapterMetasPayload', metas: Array<{ __typename?: 'ChapterMetaType', chapterId: number, key: string, value: string }> } | null, migrationMeta?: { __typename?: 'SetChapterMetasPayload', metas: Array<{ __typename?: 'ChapterMetaType', chapterId: number, key: string, value: string }> } | null }; + export type GetChaptersReaderQueryVariables = Exact<{ after?: InputMaybe; before?: InputMaybe; @@ -3987,6 +4011,18 @@ export type UpdateMangasCategoriesMutationVariables = Exact<{ export type UpdateMangasCategoriesMutation = { __typename?: 'Mutation', updateMangasCategories?: { __typename?: 'UpdateMangasCategoriesPayload', mangas: Array<{ __typename?: 'MangaType', id: number, categories: { __typename?: 'CategoryNodeList', totalCount: number, nodes: Array<{ __typename?: 'CategoryType', id: number, mangas: { __typename?: 'MangaNodeList', totalCount: number } }> } }> } | null }; +export type UpdateMangaMetadataMutationVariables = Exact<{ + updateInput: SetMangaMetasInput; + hasUpdates: Scalars['Boolean']['input']; + deleteInput: DeleteMangaMetasInput; + hasDeletions: Scalars['Boolean']['input']; + migrateInput: SetMangaMetasInput; + isMigration: Scalars['Boolean']['input']; +}>; + + +export type UpdateMangaMetadataMutation = { __typename?: 'Mutation', updatedMeta?: { __typename?: 'SetMangaMetasPayload', metas: Array<{ __typename?: 'MangaMetaType', mangaId: number, key: string, value: string }> } | null, deletedMeta?: { __typename?: 'DeleteMangaMetasPayload', metas: Array<{ __typename?: 'MangaMetaType', mangaId: number, key: string, value: string }> } | null, migrationMeta?: { __typename?: 'SetMangaMetasPayload', metas: Array<{ __typename?: 'MangaMetaType', mangaId: number, key: string, value: string }> } | null }; + export type GetMangaScreenQueryVariables = Exact<{ id: Scalars['Int']['input']; }>; @@ -4093,6 +4129,18 @@ export type SetGlobalMetadataMutationVariables = Exact<{ export type SetGlobalMetadataMutation = { __typename?: 'Mutation', setGlobalMetas?: { __typename?: 'SetGlobalMetasPayload', metas: Array<{ __typename?: 'GlobalMetaType', key: string, value: string }> } | null }; +export type UpdateGlobalMetadataMutationVariables = Exact<{ + updateInput: SetGlobalMetasInput; + hasUpdates: Scalars['Boolean']['input']; + deleteInput: DeleteGlobalMetasInput; + hasDeletions: Scalars['Boolean']['input']; + migrateInput: SetGlobalMetasInput; + isMigration: Scalars['Boolean']['input']; +}>; + + +export type UpdateGlobalMetadataMutation = { __typename?: 'Mutation', updatedMeta?: { __typename?: 'SetGlobalMetasPayload', metas: Array<{ __typename?: 'GlobalMetaType', key: string, value: string }> } | null, deletedMeta?: { __typename?: 'DeleteGlobalMetasPayload', metas: Array<{ __typename?: 'GlobalMetaType', key: string, value: string }> } | null, migrationMeta?: { __typename?: 'SetGlobalMetasPayload', metas: Array<{ __typename?: 'GlobalMetaType', key: string, value: string }> } | null }; + export type GetGlobalMetadataQueryVariables = Exact<{ key: Scalars['String']['input']; }>; @@ -4220,6 +4268,18 @@ export type DeleteSourceMetadataMutationVariables = Exact<{ export type DeleteSourceMetadataMutation = { __typename?: 'Mutation', deleteSourceMetas?: { __typename?: 'DeleteSourceMetasPayload', metas: Array<{ __typename?: 'SourceMetaType', sourceId: string, key: string, value: string }> } | null }; +export type UpdateSourceMetadataMutationVariables = Exact<{ + updateInput: SetSourceMetasInput; + hasUpdates: Scalars['Boolean']['input']; + deleteInput: DeleteSourceMetasInput; + hasDeletions: Scalars['Boolean']['input']; + migrateInput: SetSourceMetasInput; + isMigration: Scalars['Boolean']['input']; +}>; + + +export type UpdateSourceMetadataMutation = { __typename?: 'Mutation', updatedMeta?: { __typename?: 'SetSourceMetasPayload', metas: Array<{ __typename?: 'SourceMetaType', sourceId: string, key: string, value: string }> } | null, deletedMeta?: { __typename?: 'DeleteSourceMetasPayload', metas: Array<{ __typename?: 'SourceMetaType', sourceId: string, key: string, value: string }> } | null, migrationMeta?: { __typename?: 'SetSourceMetasPayload', metas: Array<{ __typename?: 'SourceMetaType', sourceId: string, key: string, value: string }> } | null }; + export type GetSourceBrowseQueryVariables = Exact<{ id: Scalars['LongString']['input']; }>; diff --git a/src/lib/graphql/manga/MangaMutation.ts b/src/lib/graphql/manga/MangaMutation.ts index 041c5217..60a2677e 100644 --- a/src/lib/graphql/manga/MangaMutation.ts +++ b/src/lib/graphql/manga/MangaMutation.ts @@ -194,3 +194,32 @@ export const UPDATE_MANGAS_CATEGORIES = gql` } } `; + +export const UPDATE_MANGA_METADATA = gql` + ${MANGA_META_FIELDS} + + mutation UPDATE_MANGA_METADATA( + $updateInput: SetMangaMetasInput! + $hasUpdates: Boolean! + $deleteInput: DeleteMangaMetasInput! + $hasDeletions: Boolean! + $migrateInput: SetMangaMetasInput! + $isMigration: Boolean! + ) { + updatedMeta: setMangaMetas(input: $updateInput) @include(if: $hasUpdates) { + metas { + ...MANGA_META_FIELDS + } + } + deletedMeta: deleteMangaMetas(input: $deleteInput) @include(if: $hasDeletions) { + metas { + ...MANGA_META_FIELDS + } + } + migrationMeta: setMangaMetas(input: $migrateInput) @include(if: $isMigration) { + metas { + ...MANGA_META_FIELDS + } + } + } +`; diff --git a/src/lib/graphql/metadata/GlobalMetadataMutation.ts b/src/lib/graphql/metadata/GlobalMetadataMutation.ts index 37e78d47..51a0781a 100644 --- a/src/lib/graphql/metadata/GlobalMetadataMutation.ts +++ b/src/lib/graphql/metadata/GlobalMetadataMutation.ts @@ -30,3 +30,31 @@ export const SET_GLOBAL_METADATA = gql` } } `; + +export const UPDATE_GLOBAL_METADATA = gql` + ${GLOBAL_METADATA} + mutation UPDATE_GLOBAL_METADATA( + $updateInput: SetGlobalMetasInput! + $hasUpdates: Boolean! + $deleteInput: DeleteGlobalMetasInput! + $hasDeletions: Boolean! + $migrateInput: SetGlobalMetasInput! + $isMigration: Boolean! + ) { + updatedMeta: setGlobalMetas(input: $updateInput) @include(if: $hasUpdates) { + metas { + ...GLOBAL_METADATA + } + } + deletedMeta: deleteGlobalMetas(input: $deleteInput) @include(if: $hasDeletions) { + metas { + ...GLOBAL_METADATA + } + } + migrationMeta: setGlobalMetas(input: $migrateInput) @include(if: $isMigration) { + metas { + ...GLOBAL_METADATA + } + } + } +`; diff --git a/src/lib/graphql/source/SourceMutation.ts b/src/lib/graphql/source/SourceMutation.ts index f966d22a..8978660a 100644 --- a/src/lib/graphql/source/SourceMutation.ts +++ b/src/lib/graphql/source/SourceMutation.ts @@ -58,3 +58,32 @@ export const DELETE_SOURCE_METADATA = gql` } } `; + +export const UPDATE_SOURCE_METADATA = gql` + ${SOURCE_META_FIELDS} + + mutation UPDATE_SOURCE_METADATA( + $updateInput: SetSourceMetasInput! + $hasUpdates: Boolean! + $deleteInput: DeleteSourceMetasInput! + $hasDeletions: Boolean! + $migrateInput: SetSourceMetasInput! + $isMigration: Boolean! + ) { + updatedMeta: setSourceMetas(input: $updateInput) @include(if: $hasUpdates) { + metas { + ...SOURCE_META_FIELDS + } + } + deletedMeta: deleteSourceMetas(input: $deleteInput) @include(if: $hasDeletions) { + metas { + ...SOURCE_META_FIELDS + } + } + migrationMeta: setSourceMetas(input: $migrateInput) @include(if: $isMigration) { + metas { + ...SOURCE_META_FIELDS + } + } + } +`; diff --git a/src/lib/requests/RequestManager.ts b/src/lib/requests/RequestManager.ts index 020619fb..62b71900 100644 --- a/src/lib/requests/RequestManager.ts +++ b/src/lib/requests/RequestManager.ts @@ -238,9 +238,31 @@ import { KoSyncLogoutMutationVariables, GetKoSyncStatusQuery, GetKoSyncStatusQueryVariables, + UpdateGlobalMetadataMutation, + UpdateGlobalMetadataMutationVariables, + UpdateMangaMetadataMutation, + UpdateMangaMetadataMutationVariables, + UpdateChapterMetadataMutation, + UpdateChapterMetadataMutationVariables, + UpdateCategoryMetadataMutation, + UpdateCategoryMetadataMutationVariables, + UpdateSourceMetadataMutation, + UpdateSourceMetadataMutationVariables, + SetCategoryMetasItemInput, + DeleteCategoryMetasItemInput, + SetSourceMetasItemInput, + DeleteSourceMetasItemInput, + SetMangaMetasItemInput, + DeleteMangaMetasItemInput, + SetChapterMetasItemInput, + DeleteChapterMetasItemInput, } from '@/lib/graphql/generated/graphql.ts'; import { GET_GLOBAL_METADATAS } from '@/lib/graphql/metadata/GlobalMetadataQuery.ts'; -import { DELETE_GLOBAL_METADATA, SET_GLOBAL_METADATA } from '@/lib/graphql/metadata/GlobalMetadataMutation.ts'; +import { + DELETE_GLOBAL_METADATA, + SET_GLOBAL_METADATA, + UPDATE_GLOBAL_METADATA, +} from '@/lib/graphql/metadata/GlobalMetadataMutation.ts'; import { CHECK_FOR_SERVER_UPDATES, CHECK_FOR_WEBUI_UPDATE, @@ -262,6 +284,7 @@ import { SET_MANGA_METADATA, UPDATE_MANGA, UPDATE_MANGA_CATEGORIES, + UPDATE_MANGA_METADATA, UPDATE_MANGAS, UPDATE_MANGAS_CATEGORIES, } from '@/lib/graphql/manga/MangaMutation.ts'; @@ -281,6 +304,7 @@ import { DELETE_SOURCE_METADATA, GET_SOURCE_MANGAS_FETCH, SET_SOURCE_METADATA, + UPDATE_SOURCE_METADATA, UPDATE_SOURCE_PREFERENCES, } from '@/lib/graphql/source/SourceMutation.ts'; import { @@ -307,6 +331,7 @@ import { GET_MANGA_CHAPTERS_FETCH, SET_CHAPTER_METADATA, UPDATE_CHAPTER, + UPDATE_CHAPTER_METADATA, UPDATE_CHAPTERS, } from '@/lib/graphql/chapter/ChapterMutation.ts'; import { @@ -315,6 +340,7 @@ import { DELETE_CATEGORY_METADATA, SET_CATEGORY_METADATA, UPDATE_CATEGORY, + UPDATE_CATEGORY_METADATA, UPDATE_CATEGORY_ORDER, } from '@/lib/graphql/category/CategoryMutation.ts'; import { STOP_UPDATER, UPDATE_LIBRARY } from '@/lib/graphql/updater/UpdaterMutation.ts'; @@ -353,6 +379,8 @@ import { SOURCE_META_FIELDS } from '@/lib/graphql/source/SourceFragments.ts'; import { CHAPTER_META_FIELDS } from '@/lib/graphql/chapter/ChapterFragments.ts'; import { MetadataMigrationSettings } from '@/features/migration/Migration.types.ts'; import { MangaIdInfo } from '@/features/manga/Manga.types.ts'; +import { ChapterIdInfo } from '@/features/chapter/Chapter.types.ts'; +import { CategoryIdInfo } from '@/features/category/Category.types.ts'; import { updateMetadataList } from '@/features/metadata/services/MetadataApolloCacheHandler.ts'; import { USER_LOGIN, USER_REFRESH } from '@/lib/graphql/user/UserMutation.ts'; import { AuthManager } from '@/features/authentication/AuthManager.ts'; @@ -361,6 +389,7 @@ import { KO_SYNC_LOGIN, KO_SYNC_LOGOUT } from '@/lib/graphql/koreader/KoreaderSy import { GET_KO_SYNC_STATUS } from '@/lib/graphql/koreader/KoreaderSyncQuery.ts'; import { ImageCache } from '@/lib/service-worker/ImageCache.ts'; import { Sources } from '@/features/source/services/Sources.ts'; +import { SourceIdInfo } from '@/features/source/Source.types.ts'; enum GQLMethod { QUERY = 'QUERY', @@ -1386,6 +1415,103 @@ export class RequestManager { ); } + public updateGlobalMeta( + { + updateInput = { metas: [] }, + deleteInput = { keys: [] }, + migrateInput = { metas: [] }, + }: { + updateInput?: SetGlobalMetasInput; + deleteInput?: DeleteGlobalMetasInput; + migrateInput?: SetGlobalMetasInput; + }, + options?: MutationOptions, + ): AbortableApolloMutationResponse { + return this.doRequest( + GQLMethod.MUTATION, + UPDATE_GLOBAL_METADATA, + { + updateInput, + hasUpdates: !!updateInput.metas.length, + deleteInput, + hasDeletions: !!deleteInput.keys?.length || !!deleteInput.prefixes?.length, + migrateInput, + isMigration: !!migrateInput.metas.length, + }, + { + optimisticResponse: { + __typename: 'Mutation', + updatedMeta: { + __typename: 'SetGlobalMetasPayload', + metas: (updateInput?.metas ?? []).map((meta) => ({ + __typename: 'GlobalMetaType', + key: meta.key, + value: meta.value, + })), + }, + deletedMeta: { + __typename: 'DeleteGlobalMetasPayload', + metas: (deleteInput?.keys ?? []).map((key) => ({ + __typename: 'GlobalMetaType' as const, + key, + value: '', + })), + }, + migrationMeta: { + __typename: 'SetGlobalMetasPayload', + metas: (migrateInput?.metas ?? []).map((meta) => ({ + __typename: 'GlobalMetaType', + key: meta.key, + value: meta.value, + })), + }, + }, + update(cache, { data }) { + deleteInput?.keys?.forEach((key) => { + cache.evict({ id: cache.identify({ __typename: 'GlobalMetaType', key }) }); + }); + + cache.modify({ + fields: { + metas(existingMetas, { readField }) { + if (!existingMetas) { + return existingMetas; + } + + if (!data?.updatedMeta && !data?.migrationMeta) { + return existingMetas; + } + + const updatedMeta = [ + ...(data?.updatedMeta?.metas ?? []), + ...(data?.migrationMeta?.metas ?? []), + ]; + + const newMetas = updatedMeta.filter((meta) => + existingMetas.nodes.every( + (existingMeta: Reference) => readField('key', existingMeta) !== meta.key, + ), + ); + const newMetaRefs = newMetas.map((meta) => + cache.writeFragment({ + data: meta, + fragment: GLOBAL_METADATA, + }), + ); + + return { + ...existingMetas, + nodes: [...existingMetas.nodes, ...newMetaRefs], + }; + }, + }, + }); + }, + ...options, + }, + ); + } + public useGetAbout( options?: QueryHookOptions, ): AbortableApolloUseQueryResponse { @@ -1810,6 +1936,104 @@ export class RequestManager { ); } + public updateSourceMeta( + sourceIds: SourceIdInfo['id'][], + { + updateInput = { metas: [] }, + deleteInput = { keys: [] }, + migrateInput = { metas: [] }, + }: { + updateInput?: Omit; + deleteInput?: Omit; + migrateInput?: Omit; + }, + options?: MutationOptions, + ): AbortableApolloMutationResponse { + return this.doRequest( + GQLMethod.MUTATION, + UPDATE_SOURCE_METADATA, + { + updateInput: { items: [{ ...updateInput, sourceIds }] }, + hasUpdates: !!updateInput.metas.length, + deleteInput: { items: [{ ...deleteInput, sourceIds }] }, + hasDeletions: !!deleteInput.keys?.length || !!deleteInput.prefixes?.length, + migrateInput: { items: [{ ...migrateInput, sourceIds }] }, + isMigration: !!migrateInput.metas.length, + }, + { + optimisticResponse: { + __typename: 'Mutation', + updatedMeta: { + __typename: 'SetSourceMetasPayload', + metas: sourceIds.flatMap((sourceId) => + (updateInput?.metas ?? []).map((meta) => ({ + __typename: 'SourceMetaType' as const, + sourceId, + key: meta.key, + value: meta.value, + })), + ), + }, + deletedMeta: { + __typename: 'DeleteSourceMetasPayload', + metas: sourceIds.flatMap((sourceId) => + (deleteInput?.keys ?? []).map((key) => ({ + __typename: 'SourceMetaType' as const, + sourceId, + key, + value: '', + })), + ), + }, + migrationMeta: { + __typename: 'SetSourceMetasPayload', + metas: sourceIds.flatMap((sourceId) => + (migrateInput?.metas ?? []).map((meta) => ({ + __typename: 'SourceMetaType' as const, + sourceId, + key: meta.key, + value: meta.value, + })), + ), + }, + }, + update(cache, { data }) { + sourceIds?.forEach((sourceId) => { + deleteInput?.keys?.forEach((key) => { + cache.evict({ id: cache.identify({ __typename: 'SourceMetaType', sourceId, key }) }); + }); + }); + + if (!data?.updatedMeta && !data?.migrationMeta) { + return; + } + + sourceIds.forEach((sourceId) => { + cache.modify({ + id: cache.identify({ __typename: 'SourceType', id: sourceId }), + fields: { + meta(existingMetas, { readField }) { + const updatedMeta = [ + ...(data?.updatedMeta?.metas ?? []), + ...(data?.migrationMeta?.metas ?? []), + ]; + + return updateMetadataList(updatedMeta, existingMetas, readField, (meta) => + cache.writeFragment({ + data: meta, + fragment: SOURCE_META_FIELDS, + }), + ); + }, + }, + }); + }); + }, + ...options, + }, + ); + } + public useGetSourceMangas( input: FetchSourceMangaInput, initialPages: number = 1, @@ -2355,6 +2579,104 @@ export class RequestManager { ); } + public updateMangaMeta( + mangaIds: MangaIdInfo['id'][], + { + updateInput = { metas: [] }, + deleteInput = { keys: [] }, + migrateInput = { metas: [] }, + }: { + updateInput?: Omit; + deleteInput?: Omit; + migrateInput?: Omit; + }, + options?: MutationOptions, + ): AbortableApolloMutationResponse { + return this.doRequest( + GQLMethod.MUTATION, + UPDATE_MANGA_METADATA, + { + updateInput: { items: [{ ...updateInput, mangaIds }] }, + hasUpdates: !!updateInput.metas.length, + deleteInput: { items: [{ ...deleteInput, mangaIds }] }, + hasDeletions: !!deleteInput.keys?.length || !!deleteInput.prefixes?.length, + migrateInput: { items: [{ ...migrateInput, mangaIds }] }, + isMigration: !!migrateInput.metas.length, + }, + { + optimisticResponse: { + __typename: 'Mutation', + updatedMeta: { + __typename: 'SetMangaMetasPayload', + metas: mangaIds.flatMap((mangaId) => + (updateInput?.metas ?? []).map((meta) => ({ + __typename: 'MangaMetaType' as const, + mangaId, + key: meta.key, + value: meta.value, + })), + ), + }, + deletedMeta: { + __typename: 'DeleteMangaMetasPayload', + metas: mangaIds.flatMap((mangaId) => + (deleteInput?.keys ?? []).map((key) => ({ + __typename: 'MangaMetaType' as const, + mangaId, + key, + value: '', + })), + ), + }, + migrationMeta: { + __typename: 'SetMangaMetasPayload', + metas: mangaIds.flatMap((mangaId) => + (migrateInput?.metas ?? []).map((meta) => ({ + __typename: 'MangaMetaType' as const, + mangaId, + key: meta.key, + value: meta.value, + })), + ), + }, + }, + update(cache, { data }) { + mangaIds.forEach((mangaId) => { + deleteInput?.keys?.forEach((key) => { + cache.evict({ id: cache.identify({ __typename: 'MangaMetaType', mangaId, key }) }); + }); + }); + + if (!data?.updatedMeta && !data?.migrationMeta) { + return; + } + + mangaIds.forEach((mangaId) => { + cache.modify({ + id: cache.identify({ __typename: 'MangaType', id: mangaId }), + fields: { + meta(existingMetas, { readField }) { + const updatedMeta = [ + ...(data?.updatedMeta?.metas ?? []), + ...(data?.migrationMeta?.metas ?? []), + ]; + + return updateMetadataList(updatedMeta, existingMetas, readField, (meta) => + cache.writeFragment({ + data: meta, + fragment: MANGA_META_FIELDS, + }), + ); + }, + }, + }); + }); + }, + ...options, + }, + ); + } + public useGetChapters( document: DocumentNode | TypedDocumentNode, variables: Variables, @@ -2606,6 +2928,104 @@ export class RequestManager { ); } + public updateChapterMeta( + chapterIds: ChapterIdInfo['id'][], + { + updateInput = { metas: [] }, + deleteInput = { keys: [] }, + migrateInput = { metas: [] }, + }: { + updateInput?: Omit; + deleteInput?: Omit; + migrateInput?: Omit; + }, + options?: MutationOptions, + ): AbortableApolloMutationResponse { + return this.doRequest( + GQLMethod.MUTATION, + UPDATE_CHAPTER_METADATA, + { + updateInput: { items: [{ ...updateInput, chapterIds }] }, + hasUpdates: !!updateInput.metas.length, + deleteInput: { items: [{ ...deleteInput, chapterIds }] }, + hasDeletions: !!deleteInput.keys?.length || !!deleteInput.prefixes?.length, + migrateInput: { items: [{ ...migrateInput, chapterIds }] }, + isMigration: !!migrateInput.metas.length, + }, + { + optimisticResponse: { + __typename: 'Mutation', + updatedMeta: { + __typename: 'SetChapterMetasPayload', + metas: chapterIds.flatMap((chapterId) => + (updateInput?.metas ?? []).map((meta) => ({ + __typename: 'ChapterMetaType' as const, + chapterId, + key: meta.key, + value: meta.value, + })), + ), + }, + deletedMeta: { + __typename: 'DeleteChapterMetasPayload', + metas: chapterIds.flatMap((chapterId) => + (deleteInput?.keys ?? []).map((key) => ({ + __typename: 'ChapterMetaType' as const, + chapterId, + key, + value: '', + })), + ), + }, + migrationMeta: { + __typename: 'SetChapterMetasPayload', + metas: chapterIds.flatMap((chapterId) => + (migrateInput?.metas ?? []).map((meta) => ({ + __typename: 'ChapterMetaType' as const, + chapterId, + key: meta.key, + value: meta.value, + })), + ), + }, + }, + update(cache, { data }) { + chapterIds.forEach((chapterId) => { + deleteInput?.keys?.forEach((key) => { + cache.evict({ id: cache.identify({ __typename: 'ChapterMetaType', chapterId, key }) }); + }); + }); + + if (!data?.updatedMeta && !data?.migrationMeta) { + return; + } + + chapterIds.forEach((chapterId) => { + cache.modify({ + id: cache.identify({ __typename: 'ChapterType', id: chapterId }), + fields: { + meta(existingMetas, { readField }) { + const updatedMeta = [ + ...(data?.updatedMeta?.metas ?? []), + ...(data?.migrationMeta?.metas ?? []), + ]; + + return updateMetadataList(updatedMeta, existingMetas, readField, (meta) => + cache.writeFragment({ + data: meta, + fragment: CHAPTER_META_FIELDS, + }), + ); + }, + }, + }); + }); + }, + ...options, + }, + ); + } + public getChapterPageUrl(mangaId: number | string, chapterIndex: number | string, page: number): string { return this.getValidImgUrlFor( `manga/${mangaId}/chapter/${chapterIndex}/page/${page}`, @@ -2910,6 +3330,104 @@ export class RequestManager { ); } + public updateCategoryMeta( + categoryIds: CategoryIdInfo['id'][], + { + updateInput = { metas: [] }, + deleteInput = { keys: [] }, + migrateInput = { metas: [] }, + }: { + updateInput?: Omit; + deleteInput?: Omit; + migrateInput?: Omit; + }, + options?: MutationOptions, + ): AbortableApolloMutationResponse { + return this.doRequest( + GQLMethod.MUTATION, + UPDATE_CATEGORY_METADATA, + { + updateInput: { items: [{ ...updateInput, categoryIds }] }, + hasUpdates: !!updateInput.metas.length, + deleteInput: { items: [{ ...deleteInput, categoryIds }] }, + hasDeletions: !!deleteInput.keys?.length || !!deleteInput.prefixes?.length, + migrateInput: { items: [{ ...migrateInput, categoryIds }] }, + isMigration: !!migrateInput.metas.length, + }, + { + optimisticResponse: { + __typename: 'Mutation', + updatedMeta: { + __typename: 'SetCategoryMetasPayload', + metas: categoryIds.flatMap((categoryId) => + (updateInput?.metas ?? []).map((meta) => ({ + __typename: 'CategoryMetaType' as const, + categoryId, + key: meta.key, + value: meta.value, + })), + ), + }, + deletedMeta: { + __typename: 'DeleteCategoryMetasPayload', + metas: categoryIds.flatMap((categoryId) => + (deleteInput?.keys ?? []).map((key) => ({ + __typename: 'CategoryMetaType' as const, + categoryId, + key, + value: '', + })), + ), + }, + migrationMeta: { + __typename: 'SetCategoryMetasPayload', + metas: categoryIds.flatMap((categoryId) => + (migrateInput?.metas ?? []).map((meta) => ({ + __typename: 'CategoryMetaType' as const, + categoryId, + key: meta.key, + value: meta.value, + })), + ), + }, + }, + update(cache, { data }) { + categoryIds.forEach((categoryId) => { + deleteInput?.keys?.forEach((key) => { + cache.evict({ id: cache.identify({ __typename: 'CategoryMetaType', categoryId, key }) }); + }); + }); + + if (!data?.updatedMeta && !data?.migrationMeta) { + return; + } + + categoryIds.forEach((categoryId) => { + cache.modify({ + id: cache.identify({ __typename: 'CategoryType', id: categoryId }), + fields: { + meta(existingMetas, { readField }) { + const updatedMeta = [ + ...(data?.updatedMeta?.metas ?? []), + ...(data?.migrationMeta?.metas ?? []), + ]; + + return updateMetadataList(updatedMeta, existingMetas, readField, (meta) => + cache.writeFragment({ + data: meta, + fragment: CATEGORY_META_FIELDS, + }), + ); + }, + }, + }); + }); + }, + ...options, + }, + ); + } + public createBackupFile( input: CreateBackupInput, options?: MutationOptions,