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.
File without changes
@@ -0,0 +1,8 @@
1
+ export { createStash } from "./client.ts";
2
+ export type { StashClient } from "./client.ts";
3
+ export { StashError } from "./transport.ts";
4
+ export type { StashOptions, Subscription, SubscriptionClient } from "./transport.ts";
5
+ export type { GraphQLRequest } from "./request.ts";
6
+ export type { Args, Field, Fields, Fragment, ObjectName, Result, RootField, Selection, SubscriptionField, } from "./types.ts";
7
+ export { STASH_VERSION } from "./generated/roots.ts";
8
+ export type * from "./generated/schema.ts";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { createStash } from "./client.js";
2
+ export { StashError } from "./transport.js";
3
+ export { STASH_VERSION } from "./generated/roots.js";
@@ -0,0 +1,23 @@
1
+ export type OperationKind = "query" | "mutation" | "subscription";
2
+ export interface GraphQLRequest {
3
+ query: string;
4
+ variables: Record<string, unknown>;
5
+ }
6
+ type FieldCall = (args?: unknown, select?: unknown) => FieldCall;
7
+ /**
8
+ * The fields of any object type: reading `fields.name` selects that field, and calling it adds
9
+ * arguments and a nested selection. It needs no schema, because the types already check usage.
10
+ */
11
+ export declare const fields: unknown;
12
+ /** Selects a field; also called by the client for root fields. */
13
+ export declare function field(name: string, args?: unknown, select?: unknown): FieldCall;
14
+ /**
15
+ * Builds one GraphQL request from selected root fields. Root arguments become variables; with
16
+ * several root fields they are prefixed with the result key so they can't collide. `upload`
17
+ * tells whether the variables may contain files.
18
+ */
19
+ export declare function buildRequest(kind: OperationKind, select: unknown): {
20
+ request: GraphQLRequest;
21
+ upload: boolean;
22
+ };
23
+ export {};
@@ -0,0 +1,131 @@
1
+ import { ROOTS } from "./generated/roots.js";
2
+ /** What each selected value stands for; the values themselves are opaque to callers. */
3
+ const nodes = new WeakMap();
4
+ /**
5
+ * The fields of any object type: reading `fields.name` selects that field, and calling it adds
6
+ * arguments and a nested selection. It needs no schema, because the types already check usage.
7
+ */
8
+ export const fields = new Proxy({}, {
9
+ get: (_, key) => typeof key !== "string"
10
+ ? undefined
11
+ : key === "$on"
12
+ ? on
13
+ : key === "$pick"
14
+ ? pick
15
+ : field(key),
16
+ });
17
+ /** Selects a field; also called by the client for root fields. */
18
+ export function field(name, args, select) {
19
+ const call = (first, second) => isCallback(first)
20
+ ? field(name, undefined, first(fields))
21
+ : field(name, first, isCallback(second) ? second(fields) : undefined);
22
+ nodes.set(call, { kind: "field", name, args, select });
23
+ return call;
24
+ }
25
+ function pick(...names) {
26
+ return Object.fromEntries(names.map((name) => [name, field(name)]));
27
+ }
28
+ function on(type, select) {
29
+ const fragment = {};
30
+ nodes.set(fragment, { kind: "fragment", type, select: select(fields) });
31
+ return { [`... on ${type}`]: fragment };
32
+ }
33
+ /**
34
+ * Builds one GraphQL request from selected root fields. Root arguments become variables; with
35
+ * several root fields they are prefixed with the result key so they can't collide. `upload`
36
+ * tells whether the variables may contain files.
37
+ */
38
+ export function buildRequest(kind, select) {
39
+ const definitions = [];
40
+ const variables = {};
41
+ let upload = false;
42
+ const selected = entries(select);
43
+ const roots = selected.map(([key, value]) => {
44
+ const node = fieldNode(key, value);
45
+ const root = ROOTS[node.name];
46
+ if (root?.[0] !== kind)
47
+ throw new Error(`${node.name} is not a ${kind} field`);
48
+ const [, types, files] = root;
49
+ upload ||= files === true;
50
+ const calls = [];
51
+ for (const [name, argument] of entries(node.args ?? {})) {
52
+ // undefined means "not passed"; null is a meaningful value and is sent as is.
53
+ if (argument === undefined)
54
+ continue;
55
+ const type = types[name];
56
+ if (type === undefined)
57
+ throw new Error(`Unknown argument ${name} on ${node.name}`);
58
+ const variable = selected.length === 1 ? name : `${key}_${name}`;
59
+ definitions.push(`$${variable}:${type}`);
60
+ calls.push(`${name}:$${variable}`);
61
+ variables[variable] = argument;
62
+ }
63
+ return selectField(key, node, calls);
64
+ });
65
+ if (roots.length === 0)
66
+ throw new Error("An operation must select at least one field");
67
+ if (kind === "subscription" && roots.length > 1) {
68
+ throw new Error("A subscription must have exactly one root field");
69
+ }
70
+ const header = definitions.length > 0 ? `(${definitions.join(",")})` : "";
71
+ const request = { query: `${kind}${header}{${roots.join(" ")}}`, variables };
72
+ return { request, upload };
73
+ }
74
+ function selectionSet(select) {
75
+ if (select === undefined)
76
+ return "";
77
+ const parts = [];
78
+ for (const [key, value] of entries(select)) {
79
+ const node = isObject(value) ? nodes.get(value) : undefined;
80
+ if (node?.kind === "fragment") {
81
+ // __typename is needed to tell union members apart.
82
+ if (!parts.includes("__typename"))
83
+ parts.push("__typename");
84
+ parts.push(`... on ${graphqlName(node.type)}${selectionSet(node.select)}`);
85
+ }
86
+ else {
87
+ const selected = fieldNode(key, value);
88
+ parts.push(selectField(key, selected, literals(selected.args)));
89
+ }
90
+ }
91
+ if (parts.length === 0)
92
+ throw new Error("A selection must select at least one field");
93
+ return `{${parts.join(" ")}}`;
94
+ }
95
+ function selectField(key, node, args) {
96
+ const alias = key === node.name ? "" : `${graphqlName(key)}:`;
97
+ const call = args.length > 0 ? `(${args.join(",")})` : "";
98
+ return `${alias}${graphqlName(node.name)}${call}${selectionSet(node.select)}`;
99
+ }
100
+ /** Nested arguments are scalars (checked by the generator), so JSON is valid GraphQL syntax. */
101
+ function literals(args) {
102
+ return entries(args ?? {})
103
+ .filter(([, argument]) => argument !== undefined)
104
+ .map(([key, argument]) => `${graphqlName(key)}:${literal(argument)}`);
105
+ }
106
+ function literal(value) {
107
+ return Array.isArray(value) ? `[${value.map(literal).join(",")}]` : JSON.stringify(value);
108
+ }
109
+ function fieldNode(key, value) {
110
+ const node = isObject(value) ? nodes.get(value) : undefined;
111
+ if (node?.kind !== "field")
112
+ throw new Error(`${key} is not a selected field`);
113
+ return node;
114
+ }
115
+ /** Keys end up in the document, so only GraphQL names are allowed. */
116
+ function graphqlName(value) {
117
+ if (!/^[_A-Za-z][_0-9A-Za-z]*$/.test(value))
118
+ throw new Error(`Invalid GraphQL name: ${value}`);
119
+ return value;
120
+ }
121
+ function entries(value) {
122
+ if (!isObject(value))
123
+ throw new Error("A selection must be an object");
124
+ return Object.entries(value);
125
+ }
126
+ function isObject(value) {
127
+ return (typeof value === "object" && value !== null) || typeof value === "function";
128
+ }
129
+ function isCallback(value) {
130
+ return typeof value === "function";
131
+ }
@@ -0,0 +1,40 @@
1
+ import type { GraphQLRequest } from "./request.ts";
2
+ /** A running subscription; `return()`, or leaving a `for await` loop, unsubscribes. */
3
+ export interface Subscription<T> extends AsyncIterableIterator<T> {
4
+ return(): Promise<IteratorResult<T, undefined>>;
5
+ }
6
+ /** Anything with the `iterate` method of a `graphql-ws` client. */
7
+ export interface SubscriptionClient {
8
+ iterate(request: GraphQLRequest): AsyncIterableIterator<unknown>;
9
+ }
10
+ export interface StashOptions {
11
+ /** GraphQL endpoint, e.g. `http://localhost:9999/graphql`. */
12
+ endpoint: string;
13
+ /** Sent as the `ApiKey` header. */
14
+ apiKey?: string | undefined;
15
+ /** Extra request headers. */
16
+ headers?: Record<string, string> | undefined;
17
+ /** Custom fetch, e.g. to add timeouts or for tests. */
18
+ fetch?: ((input: string, init: RequestInit) => Promise<Response>) | undefined;
19
+ /** Custom `graphql-ws` client for subscriptions; by default one is created on first use. */
20
+ subscriptions?: SubscriptionClient | undefined;
21
+ }
22
+ /** Thrown for non-2xx responses and GraphQL errors. */
23
+ export declare class StashError extends Error {
24
+ readonly status: number;
25
+ readonly errors: readonly {
26
+ message: string;
27
+ }[];
28
+ /** The `data` of a partial result: GraphQL can return data next to errors. */
29
+ readonly data: Record<string, unknown> | undefined;
30
+ constructor(message: string, status: number, errors?: {
31
+ message: string;
32
+ }[], data?: Record<string, unknown>);
33
+ }
34
+ /** Sends requests over HTTP and subscriptions over WebSocket; results are untyped here. */
35
+ export declare function createTransport(options: StashOptions): {
36
+ send: (request: GraphQLRequest, upload?: boolean) => Promise<Record<string, unknown>>;
37
+ /** Streams the values of root field `field`. */
38
+ subscribe(request: GraphQLRequest, field: string): Subscription<unknown>;
39
+ request: (query: string, variables?: Record<string, unknown>) => Promise<Record<string, unknown>>;
40
+ };
@@ -0,0 +1,140 @@
1
+ /** Thrown for non-2xx responses and GraphQL errors. */
2
+ export class StashError extends Error {
3
+ status;
4
+ errors;
5
+ /** The `data` of a partial result: GraphQL can return data next to errors. */
6
+ data;
7
+ constructor(message, status, errors = [], data) {
8
+ super(message);
9
+ this.name = "StashError";
10
+ this.status = status;
11
+ this.errors = errors;
12
+ this.data = data;
13
+ }
14
+ }
15
+ /** Sends requests over HTTP and subscriptions over WebSocket; results are untyped here. */
16
+ export function createTransport(options) {
17
+ const headers = {
18
+ ...(options.apiKey === undefined ? {} : { ApiKey: options.apiKey }),
19
+ ...options.headers,
20
+ };
21
+ let client;
22
+ const subscriptions = () => (client ??=
23
+ options.subscriptions === undefined
24
+ ? createSubscriptionClient(options.endpoint, headers)
25
+ : Promise.resolve(options.subscriptions));
26
+ const send = async (request, upload = false) => {
27
+ const files = [];
28
+ const variables = upload ? extractFiles(request.variables, "variables", files) : undefined;
29
+ const response = await (options.fetch ?? fetch)(options.endpoint, {
30
+ method: "POST",
31
+ // Never forward the API key to another host.
32
+ redirect: "error",
33
+ ...(files.length === 0
34
+ ? {
35
+ headers: { "content-type": "application/json", ...headers },
36
+ body: JSON.stringify(request),
37
+ }
38
+ : { headers, body: multipart(request.query, variables, files) }),
39
+ });
40
+ const payload = await response.json().catch(() => undefined);
41
+ return readData(payload, response.status, response.ok);
42
+ };
43
+ return {
44
+ send,
45
+ /** Streams the values of root field `field`. */
46
+ subscribe(request, field) {
47
+ let source;
48
+ const open = () => (source ??= subscriptions().then((subscriber) => subscriber.iterate(request)));
49
+ // Delegate instead of wrapping in an async generator: a generator only handles return()
50
+ // at its next yield, so an idle subscription could never be stopped.
51
+ return {
52
+ async next() {
53
+ const iterator = await open();
54
+ const result = await iterator.next();
55
+ if (result.done === true)
56
+ return result;
57
+ try {
58
+ return { value: readData(result.value, 200, true)[field], done: false };
59
+ }
60
+ catch (error) {
61
+ await iterator.return?.();
62
+ throw error;
63
+ }
64
+ },
65
+ async return() {
66
+ const iterator = source === undefined ? undefined : await source;
67
+ await iterator?.return?.();
68
+ return { value: undefined, done: true };
69
+ },
70
+ [Symbol.asyncIterator]() {
71
+ return this;
72
+ },
73
+ };
74
+ },
75
+ // Files may hide anywhere in a hand-written document, so always look for them.
76
+ request: (query, variables = {}) => send({ query, variables }, true),
77
+ };
78
+ }
79
+ /** Loaded on first use, so code without subscriptions never bundles graphql-ws. */
80
+ async function createSubscriptionClient(endpoint, headers) {
81
+ const { createClient } = await import("graphql-ws");
82
+ const url = endpoint.replace(/^http/, "ws");
83
+ // Browsers can't set headers on a WebSocket; they authenticate with the session cookie.
84
+ if (Object.keys(headers).length === 0)
85
+ return createClient({ url });
86
+ const NativeWebSocket = WebSocket;
87
+ class AuthenticatedWebSocket extends NativeWebSocket {
88
+ constructor(address, protocols) {
89
+ super(address, {
90
+ protocols: typeof protocols === "string" ? [protocols] : protocols,
91
+ headers,
92
+ });
93
+ }
94
+ }
95
+ return createClient({ url, webSocketImpl: AuthenticatedWebSocket });
96
+ }
97
+ function readData(payload, status, ok) {
98
+ const errors = isObject(payload) && Array.isArray(payload["errors"])
99
+ ? payload["errors"].map((error) => ({
100
+ message: isObject(error) && typeof error["message"] === "string"
101
+ ? error["message"]
102
+ : String(error),
103
+ }))
104
+ : [];
105
+ if (!ok || errors.length > 0) {
106
+ const message = errors.map((error) => error.message).join("\n");
107
+ const data = isObject(payload) && isObject(payload["data"]) ? payload["data"] : undefined;
108
+ throw new StashError(message || `HTTP ${status}`, status, errors, data);
109
+ }
110
+ return isObject(payload) && isObject(payload["data"]) ? payload["data"] : {};
111
+ }
112
+ /** GraphQL multipart request, https://github.com/jaydenseric/graphql-multipart-request-spec */
113
+ function multipart(query, variables, files) {
114
+ const form = new FormData();
115
+ form.append("operations", JSON.stringify({ query, variables }));
116
+ form.append("map", JSON.stringify(Object.fromEntries(files.map(([path], index) => [index, [path]]))));
117
+ for (const [index, [, file]] of files.entries())
118
+ form.append(String(index), file);
119
+ return form;
120
+ }
121
+ /** Replaces files with null and records their paths, as the multipart spec requires. */
122
+ function extractFiles(value, path, files) {
123
+ if (value instanceof Blob) {
124
+ files.push([path, value]);
125
+ return null;
126
+ }
127
+ if (Array.isArray(value)) {
128
+ return value.map((item, index) => extractFiles(item, `${path}.${index}`, files));
129
+ }
130
+ if (isObject(value)) {
131
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
132
+ key,
133
+ extractFiles(item, `${path}.${key}`, files),
134
+ ]));
135
+ }
136
+ return value;
137
+ }
138
+ function isObject(value) {
139
+ return typeof value === "object" && value !== null;
140
+ }
@@ -0,0 +1,120 @@
1
+ import type { Abstract, Objects } from "./generated/schema.ts";
2
+ import type { Subscription } from "./transport.ts";
3
+ /** A field with arguments `A` and return type expression `T`. */
4
+ export interface F<A, T> {
5
+ args: A;
6
+ type: T;
7
+ }
8
+ /** A list of `T`. */
9
+ export interface L<T> {
10
+ list: T;
11
+ }
12
+ /** A nullable `T`; anything not wrapped in `N` is non-null. */
13
+ export interface N<T> {
14
+ nullable: T;
15
+ }
16
+ /** A reference to an object, interface or union type. */
17
+ export interface R<Name extends keyof Objects> {
18
+ ref: Name;
19
+ }
20
+ export type ObjectName = keyof Objects;
21
+ declare const fieldValue: unique symbol;
22
+ declare const fragmentValue: unique symbol;
23
+ declare const fieldsOf: unique symbol;
24
+ /** A selected field whose value in the result is `T`. */
25
+ export interface Field<T> {
26
+ readonly [fieldValue]: T;
27
+ }
28
+ /** Fields selected on one member of a union or interface, see `$on`. */
29
+ export interface Fragment<T> {
30
+ readonly [fragmentValue]: T;
31
+ }
32
+ /** What a selection callback returns: result keys mapped to selected fields. */
33
+ export type Selection = Record<string, Field<unknown> | Fragment<unknown>>;
34
+ type Unwrap<T> = T extends N<infer I> ? Unwrap<I> : T extends L<infer I> ? Unwrap<I> : T;
35
+ /** The value of a field of type `T`; `Inner` is the value of the object it refers to. */
36
+ type Wrap<T, Inner> = T extends N<infer I> ? Wrap<I, Inner> | null : T extends L<infer I> ? Wrap<I, Inner>[] : T extends R<ObjectName> ? Inner : T;
37
+ type Value<S> = S extends Field<infer T> ? T : never;
38
+ type FragmentKey = `... on ${string}`;
39
+ type MemberOutput<S, M extends string> = S extends Record<`... on ${M}`, Fragment<infer T>> ? T : {};
40
+ /**
41
+ * The result of selection `S` on object type `Name`. The mapped types are written inline rather
42
+ * than as named helpers so that editors show the resulting object instead of a helper name.
43
+ */
44
+ export type Output<Name extends ObjectName, S> = Name extends keyof Abstract ? Extract<keyof S, FragmentKey> extends never ? {
45
+ [K in keyof S]: Value<S[K]>;
46
+ } : {
47
+ [M in Abstract[Name]]: {
48
+ [K in Exclude<keyof S, FragmentKey>]: Value<S[K]>;
49
+ } & {
50
+ __typename: M;
51
+ } & MemberOutput<S, M> extends infer O ? {
52
+ [K in keyof O]: O[K];
53
+ } : never;
54
+ }[Abstract[Name]] : {
55
+ [K in keyof S]: Value<S[K]>;
56
+ };
57
+ type Select<Name extends ObjectName, S> = (fields: Fields<Name>) => S;
58
+ type NoArgs<A> = [keyof A] extends [never] ? true : false;
59
+ type ScalarField<A, V> = NoArgs<A> extends true ? Field<V> : {} extends A ? Field<V> & ((args: A) => Field<V>) : (args: A) => Field<V>;
60
+ /** How a selected value arrives: as a nested field, a request or a subscription. */
61
+ type Delivery<Kind, V> = Kind extends "subscription" ? Subscription<V> : Kind extends "query" ? Promise<V> : Field<V>;
62
+ type ObjectField<A, T, Name extends ObjectName, Kind = "field"> = NoArgs<A> extends true ? <S extends Selection>(select: Select<Name, S>) => Delivery<Kind, Wrap<T, Output<Name, S>>> : {} extends A ? {
63
+ <S extends Selection>(select: Select<Name, S>): Delivery<Kind, Wrap<T, Output<Name, S>>>;
64
+ <S extends Selection>(args: A | undefined, select: Select<Name, S>): Delivery<Kind, Wrap<T, Output<Name, S>>>;
65
+ } : <S extends Selection>(args: A, select: Select<Name, S>) => Delivery<Kind, Wrap<T, Output<Name, S>>>;
66
+ type FieldOf<Definition> = Definition extends F<infer A, infer T> ? Unwrap<T> extends R<infer Name> ? ObjectField<A, T, Name> : ScalarField<A, Wrap<T, never>> : never;
67
+ /**
68
+ * Selects fields on member `M` of a union or interface. Spread the result into the selection;
69
+ * the result is then narrowed by `__typename`.
70
+ */
71
+ type On<Name extends keyof Abstract> = <M extends Abstract[Name] & ObjectName, S extends Selection>(type: M, select: Select<M, S>) => {
72
+ [K in `... on ${M}`]: Fragment<Output<M, S>>;
73
+ };
74
+ /** Fields that can be selected by name alone: scalars without required arguments. */
75
+ type Pickable<Name extends ObjectName> = {
76
+ [K in keyof Objects[Name]]: FieldOf<Objects[Name][K]> extends Field<unknown> ? K : never;
77
+ }[keyof Objects[Name]];
78
+ /** Selects scalar fields by name. Spread the result into a selection, or return it as is. */
79
+ type PickFields<Name extends ObjectName> = <K extends Pickable<Name>>(...names: K[]) => {
80
+ [P in K]: FieldOf<Objects[Name][P]>;
81
+ };
82
+ /**
83
+ * The fields of object type `Name`, passed to selection callbacks. Reading a field selects it;
84
+ * fields with arguments are called with them, and object fields with a nested selection.
85
+ * Don't use `...rest` on it: the object is empty at runtime, so a rest element selects nothing.
86
+ */
87
+ export type Fields<Name extends ObjectName> = {
88
+ readonly [K in keyof Objects[Name]]: FieldOf<Objects[Name][K]>;
89
+ } & {
90
+ /** The name of the object type. */
91
+ readonly __typename: Field<Name extends keyof Abstract ? Abstract[Name] : Name>;
92
+ readonly $pick: PickFields<Name>;
93
+ /** Type-only: lets `Result` find the object type of a selection callback. */
94
+ readonly [fieldsOf]?: Name;
95
+ } & (Name extends keyof Abstract ? {
96
+ readonly $on: On<Name>;
97
+ } : {});
98
+ /**
99
+ * The result of a reusable selection callback:
100
+ * `const card = (performer: Fields<"Performer">) => ({ id: performer.id })` gives
101
+ * `Result<typeof card>`, which is `{ id: string }`.
102
+ */
103
+ export type Result<Callback> = Callback extends (fields: infer Input, ...options: never[]) => infer S ? Input extends {
104
+ readonly [fieldsOf]?: infer Name extends ObjectName;
105
+ } ? Output<Name, S> : never : never;
106
+ /** All root fields; mapping over `keyof Root` keeps their docs on the client methods. */
107
+ export type Root = Objects["Query"] & Objects["Mutation"] & Objects["Subscription"];
108
+ /** Every query, mutation and subscription root field. */
109
+ export type RootField = keyof Root;
110
+ /** Root fields delivered over WebSocket. */
111
+ export type SubscriptionField = keyof Objects["Subscription"];
112
+ /** Arguments of a root field. */
113
+ export type Args<K extends RootField> = Root[K] extends F<infer A extends object, unknown> ? A : never;
114
+ type ScalarMethod<A, V, Kind> = NoArgs<A> extends true ? () => Delivery<Kind, V> : {} extends A ? (args?: A) => Delivery<Kind, V> : (args: A) => Delivery<Kind, V>;
115
+ type Method<Definition, Kind> = Definition extends F<infer A, infer T> ? Unwrap<T> extends R<infer Name> ? ObjectField<A, T, Name, Kind> : ScalarMethod<A, Wrap<T, never>, Kind> : never;
116
+ /** One method per root field: arguments first (when it has any), then the selection. */
117
+ export type RootMethods = {
118
+ [K in keyof Root]: Method<Root[K], K extends SubscriptionField ? "subscription" : "query">;
119
+ };
120
+ export {};
package/dist/types.js ADDED
File without changes
package/package.json CHANGED
@@ -1,6 +1,56 @@
1
1
  {
2
2
  "name": "stash-graphql-client",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.31.2",
4
+ "description": "Fully typed client for the Stash GraphQL API, without writing GraphQL",
5
+ "keywords": [
6
+ "graphql",
7
+ "stash",
8
+ "stashapp",
9
+ "typescript"
10
+ ],
11
+ "license": "MIT",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/shalemore/stash-graphql-client.git"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "type": "module",
20
+ "sideEffects": false,
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ }
26
+ },
27
+ "scripts": {
28
+ "lint": "oxfmt --check && oxlint",
29
+ "fix": "oxfmt && oxlint --fix",
30
+ "schema": "bun scripts/schema.ts && bun run generate",
31
+ "generate": "bun scripts/generate.ts && oxfmt schema src/generated",
32
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
33
+ "verify": "bun check && bun run lint && bun test"
34
+ },
35
+ "dependencies": {
36
+ "graphql-ws": "^6.3.0"
37
+ },
38
+ "devDependencies": {
39
+ "@octokit/rest": "^22.0.1",
40
+ "@tsconfig/bun": "^1.0.11",
41
+ "@tsconfig/strictest": "^2.0.8",
42
+ "@types/bun": "^1.4.3",
43
+ "@types/semver": "^7.8.0",
44
+ "graphql": "^17.0.2",
45
+ "oxfmt": "^0.72.0",
46
+ "oxlint": "^1.87.0",
47
+ "oxlint-tsgolint": "^7.0.2004",
48
+ "semver": "^7.8.5",
49
+ "typescript": "^7.0.2",
50
+ "zod": "^4.6.7"
51
+ },
52
+ "engines": {
53
+ "node": ">=20"
54
+ },
55
+ "packageManager": "bun@1.4.3"
56
+ }