stash-graphql-client 0.0.0-stage → 0.31.2

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,248 @@
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
+ [![npm](https://img.shields.io/npm/v/stash-graphql-client)](https://www.npmjs.com/package/stash-graphql-client)
4
+ [![CI](https://github.com/shalemore/stash-graphql-client/actions/workflows/ci.yml/badge.svg)](https://github.com/shalemore/stash-graphql-client/actions/workflows/ci.yml)
5
+ [![License](https://img.shields.io/npm/l/stash-graphql-client)](LICENSE)
6
+
7
+ A fully typed client for the [Stash](https://github.com/stashapp/stash) GraphQL API, without writing GraphQL.
8
+
9
+ > This version of the package is generated from the GraphQL schema of [Stash v0.31.1](https://github.com/stashapp/stash/releases/tag/v0.31.1).
10
+
11
+ - **Typed field selection.** Fields are selected with callbacks, and the result type contains exactly the selected fields.
12
+ - **Generated from Stash's own schema** (the latest release, see [Stash versions](#stash-versions)), so the names match.
13
+ - **Small.** Built on `fetch`, about 6 KB gzipped. The only dependency, `graphql-ws`, is loaded on the first subscription.
14
+
15
+ ```sh
16
+ npm install stash-graphql-client
17
+ ```
18
+
19
+ ## Quick start
20
+
21
+ ```ts
22
+ import { createStash } from "stash-graphql-client";
23
+
24
+ const stash = createStash({
25
+ endpoint: "http://localhost:9999/graphql",
26
+ apiKey: process.env.STASH_API_KEY,
27
+ });
28
+
29
+ // Every root field is a method: (arguments, what to select)
30
+ const { count, performers } = await stash.findPerformers(
31
+ { filter: { per_page: 20, sort: "name", direction: "ASC" } },
32
+ (result) => ({
33
+ ...result.$pick("count"),
34
+ performers: result.performers((performer) => performer.$pick("id", "name", "favorite")),
35
+ }),
36
+ );
37
+ // count: number
38
+ // performers: { id: string; name: string; favorite: boolean }[]
39
+
40
+ await stash.findPerformers((result) => result.$pick("cont"));
41
+ // ~~~~~~ unknown field
42
+ ```
43
+
44
+ ## Selecting fields
45
+
46
+ `$pick` selects scalar fields by name; an object field is called with its own selection:
47
+
48
+ ```ts
49
+ const scene = await stash.findScene({ id: "42" }, (found) => ({
50
+ ...found.$pick("id", "title"), // title: string | null, same as in the schema
51
+ studio: found.studio((studio) => studio.$pick("id", "name")),
52
+ }));
53
+ ```
54
+
55
+ A scalar can also be read directly, which selects it under the same name:
56
+
57
+ ```ts
58
+ const scene = await stash.findScene({ id: "42" }, (found) => ({
59
+ id: found.id,
60
+ title: found.title,
61
+ studio: found.studio((studio) => ({ id: studio.id, name: studio.name })),
62
+ }));
63
+ ```
64
+
65
+ Use a different key to rename a field. A field with arguments can be selected more than once under different keys:
66
+
67
+ ```ts
68
+ const { groups } = await stash.findGroups((result) => ({
69
+ groups: result.groups((group) => ({
70
+ ...group.$pick("id"),
71
+ rating: group.rating100,
72
+ directScenes: group.scene_count({ depth: 0 }),
73
+ allScenes: group.scene_count({ depth: -1 }),
74
+ })),
75
+ }));
76
+ ```
77
+
78
+ For unions and interfaces, use `$on` to pick fields of one type. `__typename` is added automatically, so the result can be narrowed on it:
79
+
80
+ ```ts
81
+ const image = await stash.findImage({ id: "7" }, (found) => ({
82
+ visual_files: found.visual_files((file) => ({
83
+ ...file.$on("ImageFile", (picture) => picture.$pick("path", "width")),
84
+ ...file.$on("VideoFile", (video) => video.$pick("path", "duration")),
85
+ })),
86
+ }));
87
+
88
+ for (const file of image?.visual_files ?? []) {
89
+ if (file.__typename === "ImageFile") console.log(file.width);
90
+ }
91
+ ```
92
+
93
+ Enums and input types are exported with the same names as in the schema:
94
+
95
+ ```ts
96
+ import type { CriterionModifier, SceneFilterType } from "stash-graphql-client";
97
+
98
+ const favorites: SceneFilterType = {
99
+ performer_favorite: true,
100
+ rating100: { value: 80, modifier: "GREATER_THAN" satisfies CriterionModifier },
101
+ };
102
+
103
+ await stash.findScenes({ scene_filter: favorites }, (result) => result.$pick("count"));
104
+ ```
105
+
106
+ ## Reusing selections
107
+
108
+ A selection is a plain function, so it can be shared like a GraphQL fragment. `Fields` and `Result` are types only and have no runtime cost:
109
+
110
+ ```ts
111
+ import type { Args, Fields, Result } from "stash-graphql-client";
112
+
113
+ const tagFields = (tag: Fields<"Tag">) => tag.$pick("id", "name");
114
+
115
+ export const performerCard = (performer: Fields<"Performer">) => ({
116
+ ...performer.$pick("id", "name"),
117
+ image: performer.image_path,
118
+ tags: performer.tags(tagFields),
119
+ });
120
+
121
+ export type PerformerCard = Result<typeof performerCard>;
122
+ // { id: string; name: string; image: string | null; tags: { id: string; name: string }[] }
123
+
124
+ // Add parameters to make it configurable
125
+ const sceneCard = (scene: Fields<"Scene">, withFiles: boolean) => ({
126
+ ...scene.$pick("id"),
127
+ tags: scene.tags(tagFields),
128
+ ...(withFiles ? { files: scene.files((file) => file.$pick("path", "size")) } : {}), // files?: ...
129
+ });
130
+
131
+ // Args is the argument type of a root field, handy for wrapping a call in your own function
132
+ const listScenes = (args: Args<"findScenes">) =>
133
+ stash.findScenes(args, (result) => ({
134
+ ...result.$pick("count"),
135
+ scenes: result.scenes((scene) => sceneCard(scene, false)),
136
+ }));
137
+ ```
138
+
139
+ Callbacks run once, only to build the request. They must return only fields, nested selections and `$on` spreads. Don't `await` or compute anything inside them; do that with the result instead.
140
+
141
+ > [!WARNING]
142
+ > Don't use `...rest` on the callback parameter. It is always empty, so nothing is selected.
143
+
144
+ ## Several root fields in one request
145
+
146
+ ```ts
147
+ const totals = await stash.query((query) => ({
148
+ scenes: query.findScenes((result) => result.$pick("count")),
149
+ favorites: query.findScenes({ scene_filter: { performer_favorite: true } }, (result) =>
150
+ result.$pick("count"),
151
+ ),
152
+ performers: query.findPerformers((result) => result.$pick("count")),
153
+ }));
154
+
155
+ totals.favorites.count; // number
156
+ ```
157
+
158
+ This sends a single HTTP request, and the keys become aliases. `mutation` works the same way and runs the fields in the order written. A query and a mutation can't share a request, and Stash doesn't accept batched HTTP, so use `Promise.all` to run them concurrently.
159
+
160
+ ## Mutations and file uploads
161
+
162
+ ```ts
163
+ import { openAsBlob } from "node:fs";
164
+
165
+ const updated = await stash.performerUpdate(
166
+ { input: { id: "3", favorite: true, rating100: 90 } },
167
+ (performer) => performer.$pick("id", "favorite"),
168
+ );
169
+
170
+ // Returns a plain boolean, so there's nothing to select
171
+ const deleted: boolean = await stash.performerDestroy({ input: { id: "3" } });
172
+
173
+ // Pass a Blob or File for uploads: openAsBlob on Node, Bun.file on Bun, or a file input in the browser
174
+ const jobId = await stash.importObjects({
175
+ input: {
176
+ file: await openAsBlob("export.zip"),
177
+ duplicateBehaviour: "IGNORE",
178
+ missingRefBehaviour: "IGNORE",
179
+ },
180
+ });
181
+ ```
182
+
183
+ ## Subscriptions
184
+
185
+ ```ts
186
+ // Returns an async iterator; leaving the loop unsubscribes
187
+ const jobs = stash.jobsSubscribe((update) => ({
188
+ ...update.$pick("type"),
189
+ job: update.job((job) => job.$pick("id", "progress")),
190
+ }));
191
+
192
+ for await (const update of jobs) {
193
+ console.log(update.type, update.job.progress);
194
+ if (update.type === "REMOVE") break;
195
+ }
196
+ ```
197
+
198
+ Subscriptions go over a WebSocket through [`graphql-ws`](https://github.com/enisdenjo/graphql-ws). The client is created on the first subscription, at the same address as `endpoint` (`http` becomes `ws`, `https` becomes `wss`).
199
+
200
+ - **Authentication.** On Node.js and Bun, `apiKey` and `headers` are sent with the WebSocket handshake too. Browsers can't set headers on a WebSocket, so there Stash has to recognize the session cookie.
201
+ - **Needs a `WebSocket`.** That is global in browsers, on Node.js 22+ and on Bun. On older versions, or to configure reconnecting, pass your own `graphql-ws` client. A custom client must also handle authentication.
202
+
203
+ ```ts
204
+ import { createClient } from "graphql-ws";
205
+
206
+ const stash = createStash({
207
+ endpoint: "http://localhost:9999/graphql",
208
+ subscriptions: createClient({
209
+ url: "ws://localhost:9999/graphql",
210
+ webSocketImpl: WebSocket, // e.g. the `ws` package on older Node.js
211
+ retryAttempts: Infinity,
212
+ }),
213
+ });
214
+ ```
215
+
216
+ ## Raw GraphQL and errors
217
+
218
+ ```ts
219
+ import { StashError } from "stash-graphql-client";
220
+
221
+ try {
222
+ // For what the typed API can't express; the result is untyped
223
+ await stash.request(`query ($id: ID!) { scene: findScene(id: $id) { id title } }`, { id: "42" });
224
+ } catch (error) {
225
+ // If part of the request worked, that data is in error.data
226
+ if (error instanceof StashError) console.error(error.status, error.errors);
227
+ }
228
+ ```
229
+
230
+ ## Options
231
+
232
+ ```ts
233
+ createStash({
234
+ endpoint: "https://stash.example.com/graphql",
235
+ apiKey: "...",
236
+ headers: { "X-Proxy-Token": "..." }, // e.g. for a reverse proxy
237
+ fetch: (url, init) => fetch(url, { ...init, signal: AbortSignal.timeout(10_000) }), // or any fetch-like function
238
+ // subscriptions: your own graphql-ws client, see Subscriptions
239
+ });
240
+ ```
241
+
242
+ ## Stash versions
243
+
244
+ Package `0.31.x` tracks the Stash `v0.31` series; patch numbers are independent. `STASH_VERSION` is the Stash release the schema comes from.
245
+
246
+ ## License
247
+
248
+ [MIT](LICENSE)
@@ -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
+ };