stash-graphql-client 0.0.0-stage → 0.31.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 shalemore
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,226 @@
1
- # Temporary Holding Version
1
+ # stash-graphql-client
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A fully typed client for the [Stash](https://github.com/stashapp/stash) GraphQL API, without writing GraphQL.
4
+
5
+ - **Pick fields by reading them.** Your editor autocompletes them, and the result type has exactly the fields you picked.
6
+ - **Generated from Stash's own schema** (latest release, see `STASH_VERSION`), so the names match.
7
+ - **No dependencies.** It just uses `fetch`, and is about 4.5 KB gzipped.
8
+
9
+ ```sh
10
+ bun add stash-graphql-client
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ ```ts
16
+ import { createStash } from "stash-graphql-client";
17
+
18
+ const stash = createStash({
19
+ endpoint: "http://localhost:9999/graphql",
20
+ apiKey: process.env.STASH_API_KEY, // sent as the ApiKey header
21
+ });
22
+
23
+ // Every root field is a method: (arguments, what to select)
24
+ const { count, performers } = await stash.findPerformers(
25
+ { filter: { per_page: 20, sort: "name", direction: "ASC" } },
26
+ (result) => ({
27
+ count: result.count,
28
+ performers: result.performers((performer) => ({
29
+ id: performer.id,
30
+ name: performer.name,
31
+ favorite: performer.favorite,
32
+ })),
33
+ }),
34
+ );
35
+ // count: number
36
+ // performers: { id: string; name: string; favorite: boolean }[]
37
+
38
+ await stash.findPerformers((result) => ({ count: result.cont }));
39
+ // ~~~~ unknown field
40
+ ```
41
+
42
+ ## Good to know
43
+
44
+ - **Looking for a field or argument?** Everything is typed, with the schema's docs as JSDoc. In the installed package, see `dist/generated/schema.d.ts` for object and input types and `dist/generated/roots.d.ts` for the root fields.
45
+ - **Read scalars, call objects.** `found.title` selects a scalar; `found.studio(...)` selects an object with a nested selection.
46
+ - **Return only selections.** The returned object may contain just selected fields, nested selections and `$on` spreads. Compute things after the request, not inside the callback.
47
+ - **Reach for plain GraphQL last.** Use `stash.request` only for something the typed API can't express.
48
+
49
+ ## Selecting fields
50
+
51
+ ```ts
52
+ const scene = await stash.findScene({ id: "42" }, (found) => ({
53
+ id: found.id,
54
+ title: found.title, // string | null, same as in the schema
55
+ studio: found.studio((studio) => ({ id: studio.id, name: studio.name })), // call it to pick nested fields
56
+ }));
57
+ ```
58
+
59
+ Want a different name? Just use a different key. Fields with arguments can even be picked twice:
60
+
61
+ ```ts
62
+ const { groups } = await stash.findGroups((result) => ({
63
+ groups: result.groups((group) => ({
64
+ id: group.id,
65
+ rating: group.rating100,
66
+ directScenes: group.scene_count({ depth: 0 }),
67
+ allScenes: group.scene_count({ depth: -1 }),
68
+ })),
69
+ }));
70
+ ```
71
+
72
+ For unions and interfaces, use `$on` to pick fields of one type. `__typename` is added for you, so you can narrow on it:
73
+
74
+ ```ts
75
+ const image = await stash.findImage({ id: "7" }, (found) => ({
76
+ visual_files: found.visual_files((file) => ({
77
+ ...file.$on("ImageFile", (picture) => ({ path: picture.path, width: picture.width })),
78
+ ...file.$on("VideoFile", (video) => ({ path: video.path, duration: video.duration })),
79
+ })),
80
+ }));
81
+
82
+ for (const file of image?.visual_files ?? []) {
83
+ if (file.__typename === "ImageFile") console.log(file.width);
84
+ }
85
+ ```
86
+
87
+ Enums and input types are exported with the same names as in the schema:
88
+
89
+ ```ts
90
+ import type { CriterionModifier, SceneFilterType } from "stash-graphql-client";
91
+
92
+ const favorites: SceneFilterType = {
93
+ performer_favorite: true,
94
+ rating100: { value: 80, modifier: "GREATER_THAN" satisfies CriterionModifier },
95
+ };
96
+
97
+ await stash.findScenes({ scene_filter: favorites }, (result) => ({ count: result.count }));
98
+ ```
99
+
100
+ ## Reusing selections
101
+
102
+ A selection is just a function, so you can share it like a fragment. `Fields` and `Result` are only types, so they cost nothing at runtime:
103
+
104
+ ```ts
105
+ import type { Args, Fields, Result } from "stash-graphql-client";
106
+
107
+ const tagFields = ({ id, name }: Fields<"Tag">) => ({ id, name });
108
+
109
+ export const performerCard = (performer: Fields<"Performer">) => ({
110
+ id: performer.id,
111
+ name: performer.name,
112
+ image: performer.image_path,
113
+ tags: performer.tags(tagFields),
114
+ });
115
+
116
+ export type PerformerCard = Result<typeof performerCard>;
117
+ // { id: string; name: string; image: string | null; tags: { id: string; name: string }[] }
118
+
119
+ type ScenesArgs = Args<"findScenes">;
120
+
121
+ // Add parameters to make it configurable
122
+ const sceneCard = (scene: Fields<"Scene">, withFiles: boolean) => ({
123
+ id: scene.id,
124
+ tags: scene.tags(tagFields),
125
+ ...(withFiles ? { files: scene.files(({ path, size }) => ({ path, size })) } : {}), // files?: ...
126
+ });
127
+ ```
128
+
129
+ Callbacks run once, just to build the request, so don't `await` or do side effects in them. Destructuring the parameter picks those fields, but `...rest` picks nothing.
130
+
131
+ ## Several root fields in one request
132
+
133
+ ```ts
134
+ const totals = await stash.query((query) => ({
135
+ scenes: query.findScenes((result) => ({ count: result.count })),
136
+ favorites: query.findScenes({ scene_filter: { performer_favorite: true } }, (result) => ({
137
+ count: result.count,
138
+ })),
139
+ performers: query.findPerformers((result) => ({ count: result.count })),
140
+ }));
141
+
142
+ totals.favorites.count; // number
143
+ ```
144
+
145
+ That's a single HTTP request, and the keys become aliases. `mutation` works the same way and runs in the order you wrote. A query and a mutation can't share a request, and Stash doesn't accept batched HTTP, so use `Promise.all` to run them side by side.
146
+
147
+ ## Mutations and file uploads
148
+
149
+ ```ts
150
+ const updated = await stash.performerUpdate(
151
+ { input: { id: "3", favorite: true, rating100: 90 } },
152
+ (performer) => ({ id: performer.id, favorite: performer.favorite }),
153
+ );
154
+
155
+ // Returns a plain boolean, so there's nothing to select
156
+ const deleted: boolean = await stash.performerDestroy({ input: { id: "3" } });
157
+
158
+ // Pass a Blob or File for uploads
159
+ const jobId = await stash.importObjects({
160
+ input: {
161
+ file: Bun.file("export.zip"),
162
+ duplicateBehaviour: "IGNORE",
163
+ missingRefBehaviour: "IGNORE",
164
+ },
165
+ });
166
+ ```
167
+
168
+ ## Subscriptions
169
+
170
+ ```ts
171
+ // Returns an async iterator; leaving the loop unsubscribes
172
+ const jobs = stash.jobsSubscribe((update) => ({
173
+ type: update.type,
174
+ job: update.job((job) => ({ id: job.id, progress: job.progress })),
175
+ }));
176
+
177
+ for await (const update of jobs) {
178
+ console.log(update.type, update.job.progress);
179
+ if (update.type === "REMOVE") break;
180
+ }
181
+ ```
182
+
183
+ ## Raw GraphQL and errors
184
+
185
+ ```ts
186
+ import { StashError } from "stash-graphql-client";
187
+
188
+ try {
189
+ // Send any GraphQL you like; the result is untyped
190
+ await stash.request(`query ($id: ID!) { scene: findScene(id: $id) { id title } }`, { id: "42" });
191
+ } catch (error) {
192
+ // If part of the request worked, that data is in error.data
193
+ if (error instanceof StashError) console.error(error.status, error.errors);
194
+ }
195
+ ```
196
+
197
+ ## Options
198
+
199
+ ```ts
200
+ createStash({
201
+ endpoint: "https://stash.example.com/graphql",
202
+ apiKey: "...",
203
+ headers: { "X-Proxy-Token": "..." }, // e.g. for a reverse proxy
204
+ fetch: (url, init) => fetch(url, { ...init, signal: AbortSignal.timeout(10_000) }), // or any fetch-like function
205
+ // subscriptions: your own graphql-ws client
206
+ });
207
+ ```
208
+
209
+ ## Development
210
+
211
+ There's a script you can run against your own server in [`example/index.ts`](example/index.ts): `STASH_URL=http://localhost:9999 STASH_API_KEY=... bun example/index.ts`.
212
+
213
+ ```sh
214
+ bun install
215
+ bun check # type check
216
+ bun run lint # oxfmt and oxlint
217
+ bun test # also type-checks the examples in this README
218
+ bun run build # writes dist
219
+ bun run schema # update to the latest Stash release (or: bun run schema v0.30.0)
220
+ ```
221
+
222
+ > [!NOTE]
223
+ > `@defer` isn't supported. Stash declares it but ignores it and always sends the full result.
224
+
225
+ > [!NOTE]
226
+ > This project was written with AI, and it runs in my own projects day to day. If you hit a bug, please [open an issue](https://github.com/shalemore/stash-graphql-client/issues/new/choose).
@@ -0,0 +1,15 @@
1
+ import { type StashOptions } from "./transport.ts";
2
+ import type { Fields, Output, RootMethods, Selection } from "./types.ts";
3
+ /** A client with one method per root field, plus `query`, `mutation` and `request`. */
4
+ export interface StashClient extends RootMethods {
5
+ /**
6
+ * Sends several queries in one request. The keys become the result keys, so the same root
7
+ * field can be selected twice with different arguments.
8
+ */
9
+ query<S extends Selection>(select: (query: Fields<"Query">) => S): Promise<Output<"Query", S>>;
10
+ /** Sends several mutations in one request; Stash runs them in order. */
11
+ mutation<S extends Selection>(select: (mutation: Fields<"Mutation">) => S): Promise<Output<"Mutation", S>>;
12
+ /** Sends any GraphQL document as is and returns the untyped `data`. */
13
+ request(query: string, variables?: Record<string, unknown>): Promise<Record<string, unknown>>;
14
+ }
15
+ export declare function createStash(options: StashOptions): StashClient;
package/dist/client.js ADDED
@@ -0,0 +1,29 @@
1
+ import { ROOTS } from "./generated/roots.js";
2
+ import { buildRequest, field, fields } from "./request.js";
3
+ import { createTransport } from "./transport.js";
4
+ export function createStash(options) {
5
+ const transport = createTransport(options);
6
+ // Async, so that errors in selection callbacks reject instead of throwing.
7
+ const send = async (kind, select) => {
8
+ const { request, upload } = buildRequest(kind, select());
9
+ return transport.send(request, upload);
10
+ };
11
+ const operation = (kind) => (select) => send(kind, () => select(fields));
12
+ const methods = Object.fromEntries(Object.entries(ROOTS).map(([name, [kind]]) => {
13
+ const root = (args, select) => ({ [name]: field(name)(args, select) });
14
+ return [
15
+ name,
16
+ kind === "subscription"
17
+ ? (args, select) => transport.subscribe(buildRequest(kind, root(args, select)).request, name)
18
+ : async (args, select) => (await send(kind, () => root(args, select)))[name],
19
+ ];
20
+ }));
21
+ const client = {
22
+ ...methods,
23
+ query: operation("query"),
24
+ mutation: operation("mutation"),
25
+ request: transport.request,
26
+ };
27
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- one method per entry in ROOTS
28
+ return client;
29
+ }
@@ -0,0 +1,6 @@
1
+ export declare const STASH_VERSION = "v0.31.1";
2
+ export declare const ROOTS: Record<string, [
3
+ "query" | "mutation" | "subscription",
4
+ Record<string, string>,
5
+ true?
6
+ ]>;
@@ -0,0 +1,281 @@
1
+ // Generated by scripts/generate.ts from the schema of Stash v0.31.1. Do not edit.
2
+ export const STASH_VERSION = "v0.31.1";
3
+ export const ROOTS = {
4
+ findSavedFilter: ["query", { id: "ID!" }],
5
+ findSavedFilters: ["query", { mode: "FilterMode" }],
6
+ findDefaultFilter: ["query", { mode: "FilterMode!" }],
7
+ findFile: ["query", { id: "ID", path: "String" }],
8
+ findFiles: ["query", { file_filter: "FileFilterType", filter: "FindFilterType", ids: "[ID!]" }],
9
+ findFolder: ["query", { id: "ID", path: "String" }],
10
+ findFolders: [
11
+ "query",
12
+ { folder_filter: "FolderFilterType", filter: "FindFilterType", ids: "[ID!]" },
13
+ ],
14
+ findScene: ["query", { id: "ID", checksum: "String" }],
15
+ findSceneByHash: ["query", { input: "SceneHashInput!" }],
16
+ findScenes: [
17
+ "query",
18
+ {
19
+ scene_filter: "SceneFilterType",
20
+ scene_ids: "[Int!]",
21
+ ids: "[ID!]",
22
+ filter: "FindFilterType",
23
+ },
24
+ ],
25
+ findScenesByPathRegex: ["query", { filter: "FindFilterType" }],
26
+ findDuplicateScenes: ["query", { distance: "Int", duration_diff: "Float" }],
27
+ sceneStreams: ["query", { id: "ID" }],
28
+ parseSceneFilenames: ["query", { filter: "FindFilterType", config: "SceneParserInput!" }],
29
+ findSceneMarkers: [
30
+ "query",
31
+ { scene_marker_filter: "SceneMarkerFilterType", filter: "FindFilterType", ids: "[ID!]" },
32
+ ],
33
+ findImage: ["query", { id: "ID", checksum: "String" }],
34
+ findImages: [
35
+ "query",
36
+ {
37
+ image_filter: "ImageFilterType",
38
+ image_ids: "[Int!]",
39
+ ids: "[ID!]",
40
+ filter: "FindFilterType",
41
+ },
42
+ ],
43
+ findPerformer: ["query", { id: "ID!" }],
44
+ findPerformers: [
45
+ "query",
46
+ {
47
+ performer_filter: "PerformerFilterType",
48
+ filter: "FindFilterType",
49
+ performer_ids: "[Int!]",
50
+ ids: "[ID!]",
51
+ },
52
+ ],
53
+ findStudio: ["query", { id: "ID!" }],
54
+ findStudios: [
55
+ "query",
56
+ { studio_filter: "StudioFilterType", filter: "FindFilterType", ids: "[ID!]" },
57
+ ],
58
+ findMovie: ["query", { id: "ID!" }],
59
+ findMovies: [
60
+ "query",
61
+ { movie_filter: "MovieFilterType", filter: "FindFilterType", ids: "[ID!]" },
62
+ ],
63
+ findGroup: ["query", { id: "ID!" }],
64
+ findGroups: [
65
+ "query",
66
+ { group_filter: "GroupFilterType", filter: "FindFilterType", ids: "[ID!]" },
67
+ ],
68
+ findGallery: ["query", { id: "ID!" }],
69
+ findGalleries: [
70
+ "query",
71
+ { gallery_filter: "GalleryFilterType", filter: "FindFilterType", ids: "[ID!]" },
72
+ ],
73
+ findTag: ["query", { id: "ID!" }],
74
+ findTags: ["query", { tag_filter: "TagFilterType", filter: "FindFilterType", ids: "[ID!]" }],
75
+ markerWall: ["query", { q: "String" }],
76
+ sceneWall: ["query", { q: "String" }],
77
+ markerStrings: ["query", { q: "String", sort: "String" }],
78
+ stats: ["query", {}],
79
+ sceneMarkerTags: ["query", { scene_id: "ID!" }],
80
+ logs: ["query", {}],
81
+ listScrapers: ["query", { types: "[ScrapeContentType!]!" }],
82
+ scrapeSingleScene: ["query", { source: "ScraperSourceInput!", input: "ScrapeSingleSceneInput!" }],
83
+ scrapeMultiScenes: ["query", { source: "ScraperSourceInput!", input: "ScrapeMultiScenesInput!" }],
84
+ scrapeSingleStudio: [
85
+ "query",
86
+ { source: "ScraperSourceInput!", input: "ScrapeSingleStudioInput!" },
87
+ ],
88
+ scrapeSingleTag: ["query", { source: "ScraperSourceInput!", input: "ScrapeSingleTagInput!" }],
89
+ scrapeSinglePerformer: [
90
+ "query",
91
+ { source: "ScraperSourceInput!", input: "ScrapeSinglePerformerInput!" },
92
+ ],
93
+ scrapeMultiPerformers: [
94
+ "query",
95
+ { source: "ScraperSourceInput!", input: "ScrapeMultiPerformersInput!" },
96
+ ],
97
+ scrapeSingleGallery: [
98
+ "query",
99
+ { source: "ScraperSourceInput!", input: "ScrapeSingleGalleryInput!" },
100
+ ],
101
+ scrapeSingleMovie: ["query", { source: "ScraperSourceInput!", input: "ScrapeSingleMovieInput!" }],
102
+ scrapeSingleGroup: ["query", { source: "ScraperSourceInput!", input: "ScrapeSingleGroupInput!" }],
103
+ scrapeSingleImage: ["query", { source: "ScraperSourceInput!", input: "ScrapeSingleImageInput!" }],
104
+ scrapeURL: ["query", { url: "String!", ty: "ScrapeContentType!" }],
105
+ scrapePerformerURL: ["query", { url: "String!" }],
106
+ scrapeSceneURL: ["query", { url: "String!" }],
107
+ scrapeGalleryURL: ["query", { url: "String!" }],
108
+ scrapeImageURL: ["query", { url: "String!" }],
109
+ scrapeMovieURL: ["query", { url: "String!" }],
110
+ scrapeGroupURL: ["query", { url: "String!" }],
111
+ plugins: ["query", {}],
112
+ pluginTasks: ["query", {}],
113
+ installedPackages: ["query", { type: "PackageType!" }],
114
+ availablePackages: ["query", { type: "PackageType!", source: "String!" }],
115
+ configuration: ["query", {}],
116
+ directory: ["query", { path: "String", locale: "String" }],
117
+ validateStashBoxCredentials: ["query", { input: "StashBoxInput!" }],
118
+ systemStatus: ["query", {}],
119
+ jobQueue: ["query", {}],
120
+ findJob: ["query", { input: "FindJobInput!" }],
121
+ dlnaStatus: ["query", {}],
122
+ allScenes: ["query", {}],
123
+ allSceneMarkers: ["query", {}],
124
+ allImages: ["query", {}],
125
+ allGalleries: ["query", {}],
126
+ allPerformers: ["query", {}],
127
+ allTags: ["query", {}],
128
+ allStudios: ["query", {}],
129
+ allMovies: ["query", {}],
130
+ version: ["query", {}],
131
+ latestversion: ["query", {}],
132
+ setup: ["mutation", { input: "SetupInput!" }],
133
+ migrate: ["mutation", { input: "MigrateInput!" }],
134
+ downloadFFMpeg: ["mutation", {}],
135
+ sceneCreate: ["mutation", { input: "SceneCreateInput!" }],
136
+ sceneUpdate: ["mutation", { input: "SceneUpdateInput!" }],
137
+ sceneMerge: ["mutation", { input: "SceneMergeInput!" }],
138
+ bulkSceneUpdate: ["mutation", { input: "BulkSceneUpdateInput!" }],
139
+ sceneDestroy: ["mutation", { input: "SceneDestroyInput!" }],
140
+ scenesDestroy: ["mutation", { input: "ScenesDestroyInput!" }],
141
+ scenesUpdate: ["mutation", { input: "[SceneUpdateInput!]!" }],
142
+ sceneIncrementO: ["mutation", { id: "ID!" }],
143
+ sceneDecrementO: ["mutation", { id: "ID!" }],
144
+ sceneAddO: ["mutation", { id: "ID!", times: "[Timestamp!]" }],
145
+ sceneDeleteO: ["mutation", { id: "ID!", times: "[Timestamp!]" }],
146
+ sceneResetO: ["mutation", { id: "ID!" }],
147
+ sceneSaveActivity: ["mutation", { id: "ID!", resume_time: "Float", playDuration: "Float" }],
148
+ sceneResetActivity: [
149
+ "mutation",
150
+ { id: "ID!", reset_resume: "Boolean", reset_duration: "Boolean" },
151
+ ],
152
+ sceneIncrementPlayCount: ["mutation", { id: "ID!" }],
153
+ sceneAddPlay: ["mutation", { id: "ID!", times: "[Timestamp!]" }],
154
+ sceneDeletePlay: ["mutation", { id: "ID!", times: "[Timestamp!]" }],
155
+ sceneResetPlayCount: ["mutation", { id: "ID!" }],
156
+ sceneGenerateScreenshot: ["mutation", { id: "ID!", at: "Float" }],
157
+ sceneMarkerCreate: ["mutation", { input: "SceneMarkerCreateInput!" }],
158
+ sceneMarkerUpdate: ["mutation", { input: "SceneMarkerUpdateInput!" }],
159
+ bulkSceneMarkerUpdate: ["mutation", { input: "BulkSceneMarkerUpdateInput!" }],
160
+ sceneMarkerDestroy: ["mutation", { id: "ID!" }],
161
+ sceneMarkersDestroy: ["mutation", { ids: "[ID!]!" }],
162
+ sceneAssignFile: ["mutation", { input: "AssignSceneFileInput!" }],
163
+ imageUpdate: ["mutation", { input: "ImageUpdateInput!" }],
164
+ bulkImageUpdate: ["mutation", { input: "BulkImageUpdateInput!" }],
165
+ imageDestroy: ["mutation", { input: "ImageDestroyInput!" }],
166
+ imagesDestroy: ["mutation", { input: "ImagesDestroyInput!" }],
167
+ imagesUpdate: ["mutation", { input: "[ImageUpdateInput!]!" }],
168
+ imageIncrementO: ["mutation", { id: "ID!" }],
169
+ imageDecrementO: ["mutation", { id: "ID!" }],
170
+ imageResetO: ["mutation", { id: "ID!" }],
171
+ galleryCreate: ["mutation", { input: "GalleryCreateInput!" }],
172
+ galleryUpdate: ["mutation", { input: "GalleryUpdateInput!" }],
173
+ bulkGalleryUpdate: ["mutation", { input: "BulkGalleryUpdateInput!" }],
174
+ galleryDestroy: ["mutation", { input: "GalleryDestroyInput!" }],
175
+ galleriesUpdate: ["mutation", { input: "[GalleryUpdateInput!]!" }],
176
+ addGalleryImages: ["mutation", { input: "GalleryAddInput!" }],
177
+ removeGalleryImages: ["mutation", { input: "GalleryRemoveInput!" }],
178
+ setGalleryCover: ["mutation", { input: "GallerySetCoverInput!" }],
179
+ resetGalleryCover: ["mutation", { input: "GalleryResetCoverInput!" }],
180
+ galleryChapterCreate: ["mutation", { input: "GalleryChapterCreateInput!" }],
181
+ galleryChapterUpdate: ["mutation", { input: "GalleryChapterUpdateInput!" }],
182
+ galleryChapterDestroy: ["mutation", { id: "ID!" }],
183
+ performerCreate: ["mutation", { input: "PerformerCreateInput!" }],
184
+ performerUpdate: ["mutation", { input: "PerformerUpdateInput!" }],
185
+ performerDestroy: ["mutation", { input: "PerformerDestroyInput!" }],
186
+ performersDestroy: ["mutation", { ids: "[ID!]!" }],
187
+ bulkPerformerUpdate: ["mutation", { input: "BulkPerformerUpdateInput!" }],
188
+ performerMerge: ["mutation", { input: "PerformerMergeInput!" }],
189
+ studioCreate: ["mutation", { input: "StudioCreateInput!" }],
190
+ studioUpdate: ["mutation", { input: "StudioUpdateInput!" }],
191
+ studioDestroy: ["mutation", { input: "StudioDestroyInput!" }],
192
+ studiosDestroy: ["mutation", { ids: "[ID!]!" }],
193
+ bulkStudioUpdate: ["mutation", { input: "BulkStudioUpdateInput!" }],
194
+ movieCreate: ["mutation", { input: "MovieCreateInput!" }],
195
+ movieUpdate: ["mutation", { input: "MovieUpdateInput!" }],
196
+ movieDestroy: ["mutation", { input: "MovieDestroyInput!" }],
197
+ moviesDestroy: ["mutation", { ids: "[ID!]!" }],
198
+ bulkMovieUpdate: ["mutation", { input: "BulkMovieUpdateInput!" }],
199
+ groupCreate: ["mutation", { input: "GroupCreateInput!" }],
200
+ groupUpdate: ["mutation", { input: "GroupUpdateInput!" }],
201
+ groupDestroy: ["mutation", { input: "GroupDestroyInput!" }],
202
+ groupsDestroy: ["mutation", { ids: "[ID!]!" }],
203
+ bulkGroupUpdate: ["mutation", { input: "BulkGroupUpdateInput!" }],
204
+ addGroupSubGroups: ["mutation", { input: "GroupSubGroupAddInput!" }],
205
+ removeGroupSubGroups: ["mutation", { input: "GroupSubGroupRemoveInput!" }],
206
+ reorderSubGroups: ["mutation", { input: "ReorderSubGroupsInput!" }],
207
+ tagCreate: ["mutation", { input: "TagCreateInput!" }],
208
+ tagUpdate: ["mutation", { input: "TagUpdateInput!" }],
209
+ tagDestroy: ["mutation", { input: "TagDestroyInput!" }],
210
+ tagsDestroy: ["mutation", { ids: "[ID!]!" }],
211
+ tagsMerge: ["mutation", { input: "TagsMergeInput!" }],
212
+ bulkTagUpdate: ["mutation", { input: "BulkTagUpdateInput!" }],
213
+ moveFiles: ["mutation", { input: "MoveFilesInput!" }],
214
+ deleteFiles: ["mutation", { ids: "[ID!]!" }],
215
+ destroyFiles: ["mutation", { ids: "[ID!]!" }],
216
+ fileSetFingerprints: ["mutation", { input: "FileSetFingerprintsInput!" }],
217
+ revealFileInFileManager: ["mutation", { id: "ID!" }],
218
+ revealFolderInFileManager: ["mutation", { id: "ID!" }],
219
+ saveFilter: ["mutation", { input: "SaveFilterInput!" }],
220
+ destroySavedFilter: ["mutation", { input: "DestroyFilterInput!" }],
221
+ setDefaultFilter: ["mutation", { input: "SetDefaultFilterInput!" }],
222
+ configureGeneral: ["mutation", { input: "ConfigGeneralInput!" }],
223
+ configureInterface: ["mutation", { input: "ConfigInterfaceInput!" }],
224
+ configureDLNA: ["mutation", { input: "ConfigDLNAInput!" }],
225
+ configureScraping: ["mutation", { input: "ConfigScrapingInput!" }],
226
+ configureDefaults: ["mutation", { input: "ConfigDefaultSettingsInput!" }],
227
+ configurePlugin: ["mutation", { plugin_id: "ID!", input: "Map!" }],
228
+ configureUI: ["mutation", { input: "Map", partial: "Map" }],
229
+ configureUISetting: ["mutation", { key: "String!", value: "Any" }],
230
+ generateAPIKey: ["mutation", { input: "GenerateAPIKeyInput!" }],
231
+ exportObjects: ["mutation", { input: "ExportObjectsInput!" }],
232
+ importObjects: ["mutation", { input: "ImportObjectsInput!" }, true],
233
+ metadataImport: ["mutation", {}],
234
+ metadataExport: ["mutation", {}],
235
+ metadataScan: ["mutation", { input: "ScanMetadataInput!" }],
236
+ metadataGenerate: ["mutation", { input: "GenerateMetadataInput!" }],
237
+ metadataAutoTag: ["mutation", { input: "AutoTagMetadataInput!" }],
238
+ metadataClean: ["mutation", { input: "CleanMetadataInput!" }],
239
+ metadataCleanGenerated: ["mutation", { input: "CleanGeneratedInput!" }],
240
+ metadataIdentify: ["mutation", { input: "IdentifyMetadataInput!" }],
241
+ migrateHashNaming: ["mutation", {}],
242
+ migrateSceneScreenshots: ["mutation", { input: "MigrateSceneScreenshotsInput!" }],
243
+ migrateBlobs: ["mutation", { input: "MigrateBlobsInput!" }],
244
+ anonymiseDatabase: ["mutation", { input: "AnonymiseDatabaseInput!" }],
245
+ optimiseDatabase: ["mutation", {}],
246
+ reloadScrapers: ["mutation", {}],
247
+ setPluginsEnabled: ["mutation", { enabledMap: "BoolMap!" }],
248
+ runPluginTask: [
249
+ "mutation",
250
+ {
251
+ plugin_id: "ID!",
252
+ task_name: "String",
253
+ description: "String",
254
+ args: "[PluginArgInput!]",
255
+ args_map: "Map",
256
+ },
257
+ ],
258
+ runPluginOperation: ["mutation", { plugin_id: "ID!", args: "Map" }],
259
+ reloadPlugins: ["mutation", {}],
260
+ installPackages: ["mutation", { type: "PackageType!", packages: "[PackageSpecInput!]!" }],
261
+ updatePackages: ["mutation", { type: "PackageType!", packages: "[PackageSpecInput!]" }],
262
+ uninstallPackages: ["mutation", { type: "PackageType!", packages: "[PackageSpecInput!]!" }],
263
+ stopJob: ["mutation", { job_id: "ID!" }],
264
+ stopAllJobs: ["mutation", {}],
265
+ submitStashBoxFingerprints: ["mutation", { input: "StashBoxFingerprintSubmissionInput!" }],
266
+ submitStashBoxSceneDraft: ["mutation", { input: "StashBoxDraftSubmissionInput!" }],
267
+ submitStashBoxPerformerDraft: ["mutation", { input: "StashBoxDraftSubmissionInput!" }],
268
+ backupDatabase: ["mutation", { input: "BackupDatabaseInput!" }],
269
+ querySQL: ["mutation", { sql: "String!", args: "[Any]" }],
270
+ execSQL: ["mutation", { sql: "String!", args: "[Any]" }],
271
+ stashBoxBatchPerformerTag: ["mutation", { input: "StashBoxBatchTagInput!" }],
272
+ stashBoxBatchStudioTag: ["mutation", { input: "StashBoxBatchTagInput!" }],
273
+ stashBoxBatchTagTag: ["mutation", { input: "StashBoxBatchTagInput!" }],
274
+ enableDLNA: ["mutation", { input: "EnableDLNAInput!" }],
275
+ disableDLNA: ["mutation", { input: "DisableDLNAInput!" }],
276
+ addTempDLNAIP: ["mutation", { input: "AddTempDLNAIPInput!" }],
277
+ removeTempDLNAIP: ["mutation", { input: "RemoveTempDLNAIPInput!" }],
278
+ jobsSubscribe: ["subscription", {}],
279
+ loggingSubscribe: ["subscription", {}],
280
+ scanCompleteSubscribe: ["subscription", {}],
281
+ };