@nxara/olio-client 0.0.0-stage → 0.1.0

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 Nxara contributors
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,281 @@
1
- # Temporary Holding Version
1
+ # @nxara/olio-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 typed GraphQL client for Nxara apps. You write operations in `.graphql` files; codegen turns them into typed documents, the way graphql-codegen does; the client gives you one call per operation:
4
+
5
+ ```ts
6
+ const { data } = await client.mutation.login({ email, password }, { headers });
7
+ ```
8
+
9
+ ```sh
10
+ bun add @nxara/olio-client graphql@16
11
+ ```
12
+
13
+ `graphql` 16 is a peer dependency (not 17, which is about 4× slower on small queries). For subscriptions, add [graphql-ws](https://github.com/enisdenjo/graphql-ws) and/or [graphql-sse](https://github.com/enisdenjo/graphql-sse): the client takes your own clients of them.
14
+
15
+ ## Operations
16
+
17
+ ```graphql
18
+ # src/operations/auth.graphql
19
+ mutation Login($email: String!, $password: String!) {
20
+ login(email: $email, password: $password) {
21
+ __typename
22
+ ... on Session {
23
+ token
24
+ }
25
+ ... on UserError {
26
+ message
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ ## Codegen
33
+
34
+ `codegenClient` reads the schema (a file, or the running server's URL by introspection) and the operations, checks the operations against the schema (errors point at `file:line:column`), and writes the types:
35
+
36
+ ```ts
37
+ import { codegenClient } from "@nxara/olio-codegen";
38
+
39
+ await codegenClient({
40
+ schema: "../server/src/schema.gql", // or "http://localhost:4000/graphql"
41
+ documents: "src/operations/*.graphql",
42
+ output: "src/generated.ts",
43
+ headers: { authorization: "Bearer …" } // only for a URL that needs it
44
+ });
45
+ ```
46
+
47
+ Per operation, named the way graphql-codegen names them: `LoginMutation` (the result), `LoginMutationVariables` and `LoginDocument`. Plus `operations`, every document by root and name, which the client takes.
48
+
49
+ ## Calls
50
+
51
+ ```ts
52
+ import { createClient } from "@nxara/olio-client";
53
+
54
+ import { operations } from "./generated.js";
55
+
56
+ const client = createClient(operations, {
57
+ url: "https://erp.example/graphql",
58
+ // Nothing before sign-in: an `undefined` value is left out, not sent as text.
59
+ headers: () => ({ authorization: token && `Bearer ${token}` })
60
+ });
61
+
62
+ const { data, errors } = await client.mutation.login({ email, password });
63
+ if (data?.login.__typename === "Session") data.login.token;
64
+ ```
65
+
66
+ - `client.<query | mutation | subscription>.<operation>(variables?, options?)`. The operation's name with its first letter lowercased: `mutation Login` is `client.mutation.login`.
67
+ - Variables are typed from the operation; they may be left out when none are required.
68
+ - `data` is the operation's result, typed from its selection: fragments narrow on `__typename`, fields under `@include` / `@skip` are optional. `errors` are the GraphQL errors as the server sent them. A response that isn't GraphQL at all (a proxy's 502 page) rejects.
69
+ - `options` takes per-call `headers` and a `signal`.
70
+ - Headers, on the client or per call, are an object, a `Headers`, or entries; in an object, `undefined` values are left out (`token ? { authorization } : {}` works too). The call's win.
71
+
72
+ ## Custom fetch
73
+
74
+ `fetch` takes anything fetch-shaped, such as a ky instance:
75
+
76
+ ```ts
77
+ import ky from "ky";
78
+
79
+ createClient(operations, { url, fetch: ky.create({ throwHttpErrors: false }) });
80
+ ```
81
+
82
+ GraphQL errors can come with 4xx/5xx statuses, so ky needs `throwHttpErrors: false`. A library that isn't fetch-shaped, such as axios, needs a few lines that return a `Response`:
83
+
84
+ ```ts
85
+ import type { Fetch } from "@nxara/olio-client";
86
+
87
+ const axiosFetch: Fetch = async (url, init) => {
88
+ const res = await axios.request({
89
+ url,
90
+ method: init.method,
91
+ headers: Object.fromEntries(new Headers(init.headers)),
92
+ data: init.body,
93
+ signal: init.signal ?? undefined,
94
+ responseType: "text",
95
+ validateStatus: () => true // GraphQL errors come with 4xx/5xx too
96
+ });
97
+ return new Response(res.data, {
98
+ status: res.status,
99
+ headers: res.headers as Record<string, string>
100
+ });
101
+ };
102
+ ```
103
+
104
+ `fetch` is for queries and mutations; subscriptions go through your own clients.
105
+
106
+ ## Uploading files
107
+
108
+ The client sends JSON, and a `File` in JSON is `{}`. `fetch` also gets the request before JSON, with the variables as you passed them, so it can send them the [multipart request spec](https://github.com/jaydenseric/graphql-multipart-request-spec)'s way instead:
109
+
110
+ ```ts
111
+ import type { Fetch } from "@nxara/olio-client";
112
+
113
+ const multipart: Fetch = (url, init, { query, variables, operationName }) => {
114
+ const files = new Map<File, string[]>();
115
+ // Files leave the variables (null in their place); `map` says where each goes back.
116
+ const strip = (value: unknown, path: string): unknown => {
117
+ if (value instanceof File) {
118
+ files.set(value, [...(files.get(value) ?? []), path]);
119
+ return null;
120
+ }
121
+ if (Array.isArray(value)) {
122
+ return value.map((v, i) => strip(v, `${path}.${i}`));
123
+ }
124
+ if (value && typeof value === "object") {
125
+ return Object.fromEntries(
126
+ Object.entries(value).map(([k, v]) => [k, strip(v, `${path}.${k}`)])
127
+ );
128
+ }
129
+ return value;
130
+ };
131
+ const stripped = strip(variables, "variables");
132
+ if (files.size === 0) return fetch(url, init);
133
+
134
+ const form = new FormData();
135
+ form.set(
136
+ "operations",
137
+ JSON.stringify({ query, variables: stripped, operationName })
138
+ );
139
+ form.set(
140
+ "map",
141
+ JSON.stringify(Object.fromEntries([...files.values()].entries()))
142
+ );
143
+ for (const [i, file] of [...files.keys()].entries())
144
+ form.set(String(i), file);
145
+
146
+ const headers = new Headers(init.headers);
147
+ headers.delete("content-type"); // fetch sets it, with the boundary
148
+ headers.set("x-upload", "1"); // browsers can't add it cross-site, so the server can refuse CSRF
149
+ return fetch(url, { ...init, headers, body: form });
150
+ };
151
+
152
+ const client = createClient(operations, { url, fetch: multipart });
153
+ await client.mutation.uploadAvatar({ file });
154
+ ```
155
+
156
+ Register the scalar as `File` (`interface ClientScalars { Upload: File }`). On the server, `@nxara/olio-http` passes graphql-http's `parseRequestParams` through, which puts the files back:
157
+
158
+ ```ts
159
+ fetchHandler(app, {
160
+ context,
161
+ parseRequestParams: async (req) => {
162
+ const request = req.raw as Request;
163
+ if (!request.headers.get("content-type")?.startsWith("multipart/form-data"))
164
+ return; // JSON, as usual
165
+ if (!request.headers.get("x-upload"))
166
+ return new Response(null, { status: 400 });
167
+ const form = await request.formData();
168
+ const params = JSON.parse(String(form.get("operations")));
169
+ const map: Record<string, string[]> = JSON.parse(String(form.get("map")));
170
+ for (const [key, paths] of Object.entries(map)) {
171
+ for (const path of paths) setPath(params, path, form.get(key)); // e.g. lodash's set
172
+ }
173
+ return params;
174
+ }
175
+ });
176
+ ```
177
+
178
+ with `scalar Upload` in the SDL, typed `File` in `CustomScalars`. Size limits are the server's (or its proxy's). Many apps skip all this: a mutation returns a signed upload URL, and the browser sends the file straight to storage.
179
+
180
+ ## Scalars
181
+
182
+ The way the server's `CustomScalars` works: one type, or `{ input; output }` when variables and results differ. Custom scalars are `unknown` until registered:
183
+
184
+ ```ts
185
+ declare module "@nxara/olio-client" {
186
+ interface ClientScalars {
187
+ DateTime: { input: Date | string; output: string };
188
+ }
189
+ }
190
+ ```
191
+
192
+ Put it in a `.ts` file, or a `.d.ts` with an `import` or `export {}`.
193
+
194
+ Enums arrive by name (`"CONFIRMED"`), not as the server's internal values.
195
+
196
+ ## Subscriptions
197
+
198
+ Over WebSocket by default ([graphql-ws](https://github.com/enisdenjo/graphql-ws)), or as server-sent events on the same URL ([graphql-sse](https://github.com/enisdenjo/graphql-sse)) for an operation marked `@sse`. The choice lives in the operation; the directive is the client's, and codegen removes it from the query the server receives:
199
+
200
+ ```graphql
201
+ subscription StockChanged($sku: String) {
202
+ # WebSocket
203
+ stockChanged(sku: $sku) {
204
+ sku
205
+ onHand
206
+ }
207
+ }
208
+
209
+ subscription Notifications @sse {
210
+ # server-sent events
211
+ notifications {
212
+ message
213
+ }
214
+ }
215
+ ```
216
+
217
+ ```ts
218
+ for await (const { data } of client.subscription.stockChanged({ sku: "CHAIR-01" })) {
219
+ console.log(data?.stockChanged.onHand);
220
+ }
221
+ ```
222
+
223
+ They go through your own [graphql-ws](https://github.com/enisdenjo/graphql-ws) and [graphql-sse](https://github.com/enisdenjo/graphql-sse) clients, which `createClient` requires as `ws` and `sse` once an operation uses that transport (and rejects when none does):
224
+
225
+ ```ts
226
+ import { createClient as createSseClient } from "graphql-sse";
227
+ import { createClient as createWsClient } from "graphql-ws";
228
+
229
+ const headers = () => ({ authorization: token && `Bearer ${token}` });
230
+ const ws = createWsClient({
231
+ url: "wss://erp.example/graphql",
232
+ // Browsers can't set headers on a WebSocket: send them here.
233
+ connectionParams: () => ({ headers: headers() })
234
+ });
235
+ const client = createClient(operations, {
236
+ url: "https://erp.example/graphql",
237
+ headers,
238
+ ws,
239
+ sse: createSseClient({ url: "https://erp.example/graphql", headers })
240
+ });
241
+ ```
242
+
243
+ The client calls their `iterate` and types the results; the rest is theirs and yours to configure: connecting, retries, keep-alive. Breaking out of the loop, or aborting `signal`, ends the subscription.
244
+
245
+ - **New credentials:** graphql-ws sends `connectionParams` each time the socket connects. After the token changes, `ws.terminate()` reconnects, and graphql-ws resubscribes live subscriptions on the new socket. It waits out its retry backoff first (`retryWait`, about 1.3 to 4 s by default), and events sent in between are lost. An open SSE stream keeps its headers until you subscribe again.
246
+ - **Refusals:** by default both libraries retry a refused connection for about 40 s before reporting it (graphql-ws on a 4403 close, graphql-sse on any response that isn't 2xx); `retryAttempts: 0` or graphql-ws's `shouldRetry` reports it at once. Then a WebSocket closed with a 4xxx code is an error result (`extensions: { code: "WEBSOCKET_CLOSED", closeCode }`), and graphql-sse throws its `NetworkError` (its `response` holds the status).
247
+ - **Rejected operations** (the server's `onSubscribe` returning errors) are an error result on both, as over HTTP.
248
+
249
+ ## Bundle size
250
+
251
+ The client imports neither library: an app bundles only the ones it creates clients with. The client's own code is about 1 KB gzipped.
252
+
253
+ ## Tests
254
+
255
+ `testClient` from `@nxara/olio-client/test` is the same client, answered in-process like Hono's `testClient`, through the app's own servers: its fetch handler for queries and mutations, and for subscriptions the graphql-ws options it passes to `useServer` (run over an in-memory socket) and its graphql-sse handler. So tests run production's context, `onConnect`, `onSubscribe`, error masking and cleanup. No server is started:
256
+
257
+ ```ts
258
+ import { testClient } from "@nxara/olio-client/test";
259
+
260
+ // The same objects the server uses.
261
+ import { graphqlHandler } from "../src/http.js";
262
+ import { sseHandler, wsOptions } from "../src/subscriptions.js";
263
+
264
+ const client = testClient(
265
+ operations,
266
+ { http: graphqlHandler, ws: wsOptions, sse: sseHandler },
267
+ { headers: { authorization: `Bearer ${token}` } }
268
+ );
269
+ expect((await client.query.me()).data).toEqual({ me: { name: "Mia" } });
270
+ ```
271
+
272
+ `ws` and `sse` are required exactly when an operation uses that transport; without subscriptions, the fetch handler alone is enough: `testClient(operations, graphqlHandler)`. Over WebSocket, the headers arrive as `connectionParams: { headers }`, and `ctx.extra` is empty (no socket or request in memory). Its clients never retry, since a failure in memory is final: a refusal over SSE throws at once. It's a separate import, so it never ends up in an app's bundle.
273
+
274
+ ## API
275
+
276
+ | | |
277
+ | --- | --- |
278
+ | `createClient(operations, { url, headers?, fetch?, ws?, sse? })` | `headers`: an object, or a function for values that change. `fetch`: default the global one; called with `(url, init, params)`, `params` being the request before JSON (see Uploading files). `ws` / `sse`: your graphql-ws / graphql-sse client, required once an operation uses that transport |
279
+ | `testClient(operations, handler \| { http, ws?, sse? }, { headers?, url? })` | From `@nxara/olio-client/test`: the same client, answered in-process |
280
+ | `ClientScalars` | Augment it with how custom scalars arrive and are sent |
281
+ | `TypedDocument`, `Result`, `Client`, `ClientOptions`, `Operations`, `SubscriptionClient` | The types behind the calls |
@@ -0,0 +1,5 @@
1
+ import type { Client, ClientOptions, Operations } from "./types.js";
2
+ export type { Fetch, HeadersSource, RequestParams } from "./transport.js";
3
+ export type { CallOptions, Client, ClientOptions, ClientScalars, GraphQLErrorJSON, Operations, Result, Scalar, SubscriptionClient, TypedDocument } from "./types.js";
4
+ export declare function createClient<O extends Operations>(operations: O, options: ClientOptions<O>): Client<O>;
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,MAAM,EACN,aAAa,EACb,UAAU,EAIX,MAAM,YAAY,CAAC;AAEpB,YAAY,EAAE,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC1E,YAAY,EACV,WAAW,EACX,MAAM,EACN,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,UAAU,EACV,MAAM,EACN,MAAM,EACN,kBAAkB,EAClB,aAAa,EACd,MAAM,YAAY,CAAC;AAYpB,wBAAgB,YAAY,CAAC,CAAC,SAAS,UAAU,EAC/C,UAAU,EAAE,CAAC,EACb,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,GACxB,MAAM,CAAC,CAAC,CAAC,CA+EX"}
package/dist/index.js ADDED
@@ -0,0 +1,58 @@
1
+ import { asResult, errorResult, resolveHeaders, stream } from "./transport.js";
2
+ const ACCEPT = "application/graphql-response+json, application/json";
3
+ export function createClient(operations, options) {
4
+ const send = options.fetch ?? ((target, init) => fetch(target, init));
5
+ const subscriptionClients = options;
6
+ const request = async (document, variables, call = {}) => {
7
+ const headers = await resolveHeaders(options.headers, call.headers);
8
+ headers.set("accept", ACCEPT);
9
+ headers.set("content-type", "application/json");
10
+ const params = {
11
+ query: document.query,
12
+ variables,
13
+ operationName: document.name
14
+ };
15
+ const response = await send(String(options.url), {
16
+ method: "POST",
17
+ headers,
18
+ body: JSON.stringify(params),
19
+ signal: call.signal
20
+ }, params);
21
+ if (!response.headers.get("content-type")?.includes("json")) {
22
+ throw new Error(`${document.name}: HTTP ${response.status} ${response.statusText}`.trim());
23
+ }
24
+ return asResult((await response.json()));
25
+ };
26
+ async function* subscribe(document, variables, signal) {
27
+ const kind = document.transport ?? "ws";
28
+ const client = subscriptionClients[kind];
29
+ if (!client) {
30
+ throw new Error(`${document.name} subscribes over ${kind}: pass createClient a graphql-${kind} client as \`${kind}\``);
31
+ }
32
+ const events = client.iterate({
33
+ query: document.query,
34
+ variables: variables,
35
+ operationName: document.name
36
+ });
37
+ try {
38
+ for await (const event of stream(events, signal))
39
+ yield asResult(event);
40
+ }
41
+ catch (error) {
42
+ const result = errorResult(error);
43
+ if (!result)
44
+ throw error;
45
+ yield result;
46
+ }
47
+ }
48
+ return Object.fromEntries(["query", "mutation", "subscription"].map((root) => [
49
+ root,
50
+ Object.fromEntries(Object.entries((operations[root] ?? {})).map(([name, document]) => [
51
+ name,
52
+ (variables, call) => root === "subscription"
53
+ ? subscribe(document, variables, call?.signal)
54
+ : request(document, variables, call)
55
+ ]))
56
+ ]));
57
+ }
58
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/test.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import type { ServerOptions } from "graphql-ws";
2
+ import { type HeadersSource, type MaybePromise } from "./transport.js";
3
+ import type { Client, Operations, Transports } from "./types.js";
4
+ type Handler = (request: Request) => MaybePromise<Response>;
5
+ export type TestServer<O extends Operations = Operations> = {
6
+ http: Handler;
7
+ } & {
8
+ readonly [K in Transports<O>]: K extends "ws" ? ServerOptions<never, never> : Handler;
9
+ };
10
+ export declare function testClient<O extends Operations>(operations: O, server: TestServer<O> | ([Transports<O>] extends [never] ? Handler : never), options?: {
11
+ url?: string | URL;
12
+ headers?: HeadersSource;
13
+ }): Client<O>;
14
+ export {};
15
+ //# sourceMappingURL=test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../src/test.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGhD,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,MAAM,EAAiB,UAAU,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEhF,KAAK,OAAO,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,YAAY,CAAC,QAAQ,CAAC,CAAC;AAmB5D,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,UAAU,GAAG,UAAU,IAAI;IAE1D,IAAI,EAAE,OAAO,CAAC;CACf,GAAG;IACF,QAAQ,EAAE,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,IAAI,GAKzC,aAAa,CAAC,KAAK,EAAE,KAAK,CAAC,GAE3B,OAAO;CACZ,CAAC;AAuFF,wBAAgB,UAAU,CAAC,CAAC,SAAS,UAAU,EAC7C,UAAU,EAAE,CAAC,EACb,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,OAAO,GAAG,KAAK,CAAC,EAC3E,OAAO,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;IAAC,OAAO,CAAC,EAAE,aAAa,CAAA;CAAO,GAC5D,MAAM,CAAC,CAAC,CAAC,CA6CX"}
package/dist/test.js ADDED
@@ -0,0 +1,97 @@
1
+ import { createClient } from "./index.js";
2
+ import { resolveHeaders } from "./transport.js";
3
+ const graphqlWs = await import("graphql-ws").catch(() => undefined);
4
+ const graphqlSse = await import("graphql-sse").catch(() => undefined);
5
+ const installed = (module, name) => {
6
+ if (!module) {
7
+ throw new Error(`testClient: install ${name} to test subscriptions over it`);
8
+ }
9
+ return module;
10
+ };
11
+ function memoryWebSocket(ws, options) {
12
+ const graphqlWs = ws.makeServer(options);
13
+ return class Socket {
14
+ static CONNECTING = 0;
15
+ static OPEN = 1;
16
+ static CLOSING = 2;
17
+ static CLOSED = 3;
18
+ readyState = Socket.CONNECTING;
19
+ protocol = ws.GRAPHQL_TRANSPORT_WS_PROTOCOL;
20
+ onopen = null;
21
+ onmessage = null;
22
+ onclose = null;
23
+ onerror = null;
24
+ #receive;
25
+ #closed;
26
+ constructor() {
27
+ queueMicrotask(() => {
28
+ this.#closed = graphqlWs.opened({
29
+ protocol: this.protocol,
30
+ send: (data) => {
31
+ if (this.readyState !== Socket.OPEN)
32
+ return;
33
+ queueMicrotask(() => this.onmessage?.({ data }));
34
+ },
35
+ close: (code, reason) => this.#finish(code, reason),
36
+ onMessage: (receive) => {
37
+ this.#receive = receive;
38
+ }
39
+ }, {});
40
+ this.readyState = Socket.OPEN;
41
+ this.onopen?.({});
42
+ });
43
+ }
44
+ send(data) {
45
+ queueMicrotask(() => {
46
+ this.#receive?.(data).catch((error) => this.#finish(ws.CloseCode.InternalServerError, error instanceof Error ? error.message : String(error)));
47
+ });
48
+ }
49
+ close(code = 1000, reason = "") {
50
+ this.#finish(code, reason);
51
+ }
52
+ #finish(code = 1000, reason = "") {
53
+ if (this.readyState === Socket.CLOSED)
54
+ return;
55
+ this.readyState = Socket.CLOSED;
56
+ void this.#closed?.(code, reason);
57
+ queueMicrotask(() => this.onclose?.({ code, reason, wasClean: true }));
58
+ }
59
+ };
60
+ }
61
+ export function testClient(operations, server, options = {}) {
62
+ const handlers = (typeof server === "function" ? { http: server } : server);
63
+ for (const kind of operations.transports ?? []) {
64
+ if (!handlers[kind]) {
65
+ throw new Error(`These operations subscribe over ${kind}: pass testClient(operations, { http, ${kind} }) the app's own ${kind === "ws" ? "graphql-ws server options" : "graphql-sse handler"}`);
66
+ }
67
+ }
68
+ const url = String(options.url ?? "http://localhost/graphql");
69
+ const headers = async () => Object.fromEntries(await resolveHeaders(options.headers));
70
+ const fetch = async (target, init) => {
71
+ const request = new Request(target, init);
72
+ const streams = request.headers
73
+ .get("accept")
74
+ ?.includes("text/event-stream");
75
+ return handlers.sse && streams
76
+ ? handlers.sse(request)
77
+ : handlers.http(request);
78
+ };
79
+ const ws = handlers.ws && installed(graphqlWs, "graphql-ws");
80
+ const sse = handlers.sse && installed(graphqlSse, "graphql-sse");
81
+ return createClient(operations, {
82
+ url,
83
+ headers: options.headers,
84
+ fetch,
85
+ ws: ws &&
86
+ handlers.ws &&
87
+ ws.createClient({
88
+ url: url.replace(/^http/, "ws"),
89
+ webSocketImpl: memoryWebSocket(ws, handlers.ws),
90
+ retryAttempts: 0,
91
+ connectionParams: async () => ({ headers: await headers() })
92
+ }),
93
+ sse: sse &&
94
+ sse.createClient({ url, fetchFn: fetch, headers, retryAttempts: 0 })
95
+ });
96
+ }
97
+ //# sourceMappingURL=test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test.js","sourceRoot":"","sources":["../src/test.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EACL,cAAc,EAGf,MAAM,gBAAgB,CAAC;AAMxB,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;AACpE,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;AAEtE,MAAM,SAAS,GAAG,CAAI,MAAqB,EAAE,IAAY,EAAK,EAAE;IAC9D,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CACb,uBAAuB,IAAI,gCAAgC,CAC5D,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC;AA0BF,SAAS,eAAe,CACtB,EAA+B,EAC/B,OAAoC;IAEpC,MAAM,SAAS,GAAG,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IAIzC,OAAO,MAAM,MAAM;QACjB,MAAM,CAAU,UAAU,GAAG,CAAC,CAAC;QAC/B,MAAM,CAAU,IAAI,GAAG,CAAC,CAAC;QACzB,MAAM,CAAU,OAAO,GAAG,CAAC,CAAC;QAC5B,MAAM,CAAU,MAAM,GAAG,CAAC,CAAC;QAE3B,UAAU,GAAW,MAAM,CAAC,UAAU,CAAC;QAC9B,QAAQ,GAAG,EAAE,CAAC,6BAA6B,CAAC;QACrD,MAAM,GAAa,IAAI,CAAC;QACxB,SAAS,GAAa,IAAI,CAAC;QAC3B,OAAO,GAAa,IAAI,CAAC;QACzB,OAAO,GAAa,IAAI,CAAC;QACzB,QAAQ,CAAgD;QACxD,OAAO,CAAkE;QAEzE;YACE,cAAc,CAAC,GAAG,EAAE;gBAClB,IAAI,CAAC,OAAO,GAAG,SAAS,CAAC,MAAM,CAC7B;oBACE,QAAQ,EAAE,IAAI,CAAC,QAAQ;oBACvB,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE;wBACb,IAAI,IAAI,CAAC,UAAU,KAAK,MAAM,CAAC,IAAI;4BAAE,OAAO;wBAC5C,cAAc,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,EAAE,IAAI,EAAW,CAAC,CAAC,CAAC;oBAC5D,CAAC;oBACD,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;oBACnD,SAAS,EAAE,CAAC,OAAO,EAAE,EAAE;wBACrB,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;oBAC1B,CAAC;iBACF,EAED,EAAW,CACZ,CAAC;gBACF,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC;gBAC9B,IAAI,CAAC,MAAM,EAAE,CAAC,EAAW,CAAC,CAAC;YAC7B,CAAC,CAAC,CAAC;QACL,CAAC;QAED,IAAI,CAAC,IAAY;YACf,cAAc,CAAC,GAAG,EAAE;gBAElB,IAAI,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE,CAC7C,IAAI,CAAC,OAAO,CACV,EAAE,CAAC,SAAS,CAAC,mBAAmB,EAChC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CACvD,CACF,CAAC;YACJ,CAAC,CAAC,CAAC;QACL,CAAC;QAED,KAAK,CAAC,IAAI,GAAG,IAAI,EAAE,MAAM,GAAG,EAAE;YAC5B,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC7B,CAAC;QAED,OAAO,CAAC,IAAI,GAAG,IAAI,EAAE,MAAM,GAAG,EAAE;YAC9B,IAAI,IAAI,CAAC,UAAU,KAAK,MAAM,CAAC,MAAM;gBAAE,OAAO;YAC9C,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC;YAChC,KAAK,IAAI,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YAClC,cAAc,CAAC,GAAG,EAAE,CAClB,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAW,CAAC,CAC1D,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC;AASD,MAAM,UAAU,UAAU,CACxB,UAAa,EACb,MAA2E,EAC3E,OAAO,GAAoD,EAAE;IAE7D,MAAM,QAAQ,GAAG,CACf,OAAO,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,MAAM,CACa,CAAC;IACxE,KAAK,MAAM,IAAI,IAAI,UAAU,CAAC,UAAU,IAAI,EAAE,EAAE,CAAC;QAC/C,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CACb,mCAAmC,IAAI,yCAAyC,IAAI,qBAAqB,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,2BAA2B,CAAC,CAAC,CAAC,qBAAqB,EAAE,CAC/K,CAAC;QACJ,CAAC;IACH,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,IAAI,0BAA0B,CAAC,CAAC;IAC9D,MAAM,OAAO,GAAG,KAAK,IAAqC,EAAE,CAC1D,MAAM,CAAC,WAAW,CAAC,MAAM,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5D,MAAM,KAAK,GAAG,KAAK,EACjB,MAAc,EACd,IAAiB,EACE,EAAE;QACrB,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAC1C,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO;aAC5B,GAAG,CAAC,QAAQ,CAAC;YACd,EAAE,QAAQ,CAAC,mBAAmB,CAAC,CAAC;QAClC,OAAO,QAAQ,CAAC,GAAG,IAAI,OAAO;YAC5B,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC;YACvB,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC,CAAC;IACF,MAAM,EAAE,GAAG,QAAQ,CAAC,EAAE,IAAI,SAAS,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;IAC7D,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,IAAI,SAAS,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;IACjE,OAAO,YAAY,CAAC,UAAU,EAAE;QAC9B,GAAG;QACH,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,KAAK;QACL,EAAE,EACA,EAAE;YACF,QAAQ,CAAC,EAAE;YACX,EAAE,CAAC,YAAY,CAAC;gBACd,GAAG,EAAE,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC;gBAC/B,aAAa,EAAE,eAAe,CAAC,EAAE,EAAE,QAAQ,CAAC,EAAE,CAAC;gBAC/C,aAAa,EAAE,CAAC;gBAChB,gBAAgB,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,OAAO,EAAE,EAAE,CAAC;aAC7D,CAAC;QACJ,GAAG,EACD,GAAG;YACH,GAAG,CAAC,YAAY,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,EAAE,CAAC;KACxC,CAAC,CAAC;AACpC,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { GraphQLErrorJSON, HeadersValue, Result } from "./types.js";
2
+ export interface RequestParams {
3
+ query: string;
4
+ variables: unknown;
5
+ operationName: string;
6
+ }
7
+ export type Fetch = (url: string, init: RequestInit, params: RequestParams) => Promise<Response>;
8
+ export type MaybePromise<T> = T | Promise<T>;
9
+ export type HeadersSource = HeadersValue | (() => MaybePromise<HeadersValue>);
10
+ export declare const asResult: (body: {
11
+ data?: unknown;
12
+ errors?: readonly GraphQLErrorJSON[];
13
+ }) => Result<never>;
14
+ export declare function resolveHeaders(...sources: (HeadersSource | undefined)[]): Promise<Headers>;
15
+ export declare function stream<T>(events: AsyncIterableIterator<T>, signal: AbortSignal | undefined): AsyncGenerator<T>;
16
+ export declare function errorResult(error: unknown): Result<never> | undefined;
17
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzE,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,OAAO,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;CACvB;AAKD,MAAM,MAAM,KAAK,GAAG,CAClB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,aAAa,KAClB,OAAO,CAAC,QAAQ,CAAC,CAAC;AACvB,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAE7C,MAAM,MAAM,aAAa,GAAG,YAAY,GAAG,CAAC,MAAM,YAAY,CAAC,YAAY,CAAC,CAAC,CAAC;AAE9E,eAAO,MAAM,QAAQ,SAAU;IAC7B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;CACtC,KAAG,MAAM,CAAC,KAAK,CAGd,CAAC;AAkBH,wBAAsB,cAAc,CAClC,GAAG,OAAO,EAAE,CAAC,aAAa,GAAG,SAAS,CAAC,EAAE,GACxC,OAAO,CAAC,OAAO,CAAC,CASlB;AAGD,wBAAuB,MAAM,CAAC,CAAC,EAC7B,MAAM,EAAE,qBAAqB,CAAC,CAAC,CAAC,EAChC,MAAM,EAAE,WAAW,GAAG,SAAS,GAC9B,cAAc,CAAC,CAAC,CAAC,CAQnB;AAkBD,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,SAAS,CAcrE"}
@@ -0,0 +1,52 @@
1
+ export const asResult = (body) => ({
2
+ data: (body.data ?? null),
3
+ ...(body.errors ? { errors: body.errors } : {})
4
+ });
5
+ function entries(headers) {
6
+ if (headers === undefined ||
7
+ Array.isArray(headers) ||
8
+ headers instanceof Headers) {
9
+ return new Headers(headers);
10
+ }
11
+ return Object.entries(headers).filter((entry) => entry[1] !== undefined);
12
+ }
13
+ export async function resolveHeaders(...sources) {
14
+ const resolved = await Promise.all(sources.map((source) => (typeof source === "function" ? source() : source)));
15
+ const merged = new Headers();
16
+ for (const headers of resolved) {
17
+ for (const [key, value] of entries(headers))
18
+ merged.set(key, value);
19
+ }
20
+ return merged;
21
+ }
22
+ export async function* stream(events, signal) {
23
+ const stop = () => void events.return?.();
24
+ signal?.addEventListener("abort", stop, { once: true });
25
+ try {
26
+ yield* events;
27
+ }
28
+ finally {
29
+ signal?.removeEventListener("abort", stop);
30
+ }
31
+ }
32
+ const isClose = (error) => typeof error === "object" &&
33
+ error !== null &&
34
+ "code" in error &&
35
+ "reason" in error;
36
+ export function errorResult(error) {
37
+ if (Array.isArray(error)) {
38
+ return asResult({ errors: error });
39
+ }
40
+ if (!isClose(error) || error.code < 4000 || error.code > 4999)
41
+ return;
42
+ return {
43
+ data: null,
44
+ errors: [
45
+ {
46
+ message: error.reason || `WebSocket closed (${error.code})`,
47
+ extensions: { code: "WEBSOCKET_CLOSED", closeCode: error.code }
48
+ }
49
+ ]
50
+ };
51
+ }
52
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAoBA,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,IAGxB,EAAiB,EAAE,CAAC,CAAC;IACpB,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAU;IAClC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;CAChD,CAAC,CAAC;AAGH,SAAS,OAAO,CACd,OAAiC;IAEjC,IACE,OAAO,KAAK,SAAS;QACrB,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QACtB,OAAO,YAAY,OAAO,EAC1B,CAAC;QACD,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9B,CAAC;IACD,OAAO,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,CACnC,CAAC,KAAK,EAA6B,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,SAAS,CAC7D,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,GAAG,OAAsC;IAEzC,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,CAChC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAC5E,CAAC;IACF,MAAM,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;IAC7B,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC;YAAE,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAGD,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,MAAM,CAC3B,MAAgC,EAChC,MAA+B;IAE/B,MAAM,IAAI,GAAG,GAAS,EAAE,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC;IAChD,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IACxD,IAAI,CAAC;QACH,KAAK,CAAC,CAAC,MAAM,CAAC;IAChB,CAAC;YAAS,CAAC;QACT,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAC7C,CAAC;AACH,CAAC;AAOD,MAAM,OAAO,GAAG,CAAC,KAAc,EAAuB,EAAE,CACtD,OAAO,KAAK,KAAK,QAAQ;IACzB,KAAK,KAAK,IAAI;IACd,MAAM,IAAI,KAAK;IACf,QAAQ,IAAI,KAAK,CAAC;AAOpB,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,QAAQ,CAAC,EAAE,MAAM,EAAE,KAA2B,EAAE,CAAC,CAAC;IAC3D,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,GAAG,IAAI,IAAI,KAAK,CAAC,IAAI,GAAG,IAAI;QAAE,OAAO;IACtE,OAAO;QACL,IAAI,EAAE,IAAI;QACV,MAAM,EAAE;YACN;gBACE,OAAO,EAAE,KAAK,CAAC,MAAM,IAAI,qBAAqB,KAAK,CAAC,IAAI,GAAG;gBAC3D,UAAU,EAAE,EAAE,IAAI,EAAE,kBAAkB,EAAE,SAAS,EAAE,KAAK,CAAC,IAAI,EAAE;aAChE;SACF;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,88 @@
1
+ import type { Fetch, HeadersSource } from "./transport.js";
2
+ export interface ClientScalars {
3
+ }
4
+ interface DefaultScalars {
5
+ ID: string;
6
+ String: string;
7
+ Int: number;
8
+ Float: number;
9
+ Boolean: boolean;
10
+ }
11
+ export type Scalar<N extends string, D extends "input" | "output" = "output"> = N extends keyof ClientScalars ? ClientScalars[N] extends {
12
+ input: infer I;
13
+ output: infer O;
14
+ } ? D extends "input" ? I : O : ClientScalars[N] : N extends keyof DefaultScalars ? DefaultScalars[N] : unknown;
15
+ export type Operation = "query" | "mutation" | "subscription";
16
+ export interface TypedDocument<Result, Variables, Op extends Operation = Operation> {
17
+ readonly operation: Op;
18
+ readonly name: string;
19
+ readonly query: string;
20
+ readonly transport?: "ws" | "sse";
21
+ readonly "~types"?: {
22
+ result: Result;
23
+ variables: Variables;
24
+ };
25
+ }
26
+ export interface GraphQLErrorJSON {
27
+ message: string;
28
+ locations?: readonly {
29
+ line: number;
30
+ column: number;
31
+ }[];
32
+ path?: readonly (string | number)[];
33
+ extensions?: Record<string, unknown>;
34
+ }
35
+ export interface Result<T> {
36
+ data: T | null;
37
+ errors?: readonly GraphQLErrorJSON[];
38
+ }
39
+ export type HeadersInit = ConstructorParameters<typeof Headers>[0];
40
+ export type HeadersValue = HeadersInit | Readonly<Record<string, string | undefined>>;
41
+ export interface CallOptions {
42
+ signal?: AbortSignal;
43
+ headers?: HeadersValue;
44
+ }
45
+ type RequiredKeys<T> = {
46
+ [K in keyof T]-?: object extends Pick<T, K> ? never : K;
47
+ }[keyof T];
48
+ export type VariablesArgs<V, O> = [RequiredKeys<V>] extends [never] ? [variables?: V, options?: O] : [variables: V, options?: O];
49
+ interface AnyDocument<Op extends Operation> {
50
+ readonly operation: Op;
51
+ readonly name: string;
52
+ readonly query: string;
53
+ readonly transport?: "ws" | "sse";
54
+ }
55
+ export interface Operations {
56
+ readonly query?: Readonly<Record<string, AnyDocument<"query">>>;
57
+ readonly mutation?: Readonly<Record<string, AnyDocument<"mutation">>>;
58
+ readonly subscription?: Readonly<Record<string, AnyDocument<"subscription">>>;
59
+ readonly transports?: readonly ("ws" | "sse")[];
60
+ }
61
+ export interface SubscriptionClient {
62
+ iterate(request: {
63
+ query: string;
64
+ variables?: Record<string, unknown>;
65
+ operationName?: string;
66
+ }): AsyncIterableIterator<{
67
+ data?: unknown;
68
+ errors?: readonly GraphQLErrorJSON[];
69
+ }>;
70
+ }
71
+ type Call<D> = D extends TypedDocument<infer R, infer V, infer Op> ? Op extends "subscription" ? (...args: VariablesArgs<V, Pick<CallOptions, "signal">>) => AsyncIterableIterator<Result<R>> : (...args: VariablesArgs<V, CallOptions>) => Promise<Result<R>> : never;
72
+ export type Client<O extends Operations> = {
73
+ readonly [Op in keyof O & Operation]: {
74
+ readonly [K in keyof O[Op]]: Call<O[Op][K]>;
75
+ };
76
+ };
77
+ export type Transports<O extends Operations> = O extends {
78
+ transports: readonly (infer K)[];
79
+ } ? K & ("ws" | "sse") : never;
80
+ export type ClientOptions<O extends Operations> = {
81
+ url: string | URL;
82
+ headers?: HeadersSource;
83
+ fetch?: Fetch;
84
+ } & {
85
+ readonly [K in Transports<O>]: SubscriptionClient;
86
+ };
87
+ export {};
88
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,62 @@
1
1
  {
2
2
  "name": "@nxara/olio-client",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "A typed GraphQL client for Nxara apps: select fields, get typed results.",
5
+ "keywords": [
6
+ "client",
7
+ "graphql",
8
+ "nxara",
9
+ "rpc"
10
+ ],
11
+ "license": "MIT",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/nxara-hub/olio.git",
15
+ "directory": "packages/client"
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "type": "module",
21
+ "sideEffects": false,
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "default": "./dist/index.js"
26
+ },
27
+ "./test": {
28
+ "types": "./dist/test.d.ts",
29
+ "default": "./dist/test.js"
30
+ }
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "scripts": {
36
+ "build": "tsc",
37
+ "test": "bun test",
38
+ "check-types": "bun test/generate.ts && tsc -p tsconfig.test.json"
39
+ },
40
+ "devDependencies": {
41
+ "@nxara/olio": "0.1.0",
42
+ "@nxara/olio-codegen": "0.1.0",
43
+ "@nxara/olio-http": "0.1.0",
44
+ "graphql": "^16.14.2",
45
+ "graphql-sse": "2.6.3",
46
+ "graphql-ws": "^6.4.1",
47
+ "ky": "^2.2.0"
48
+ },
49
+ "peerDependencies": {
50
+ "graphql": "^16.13.0",
51
+ "graphql-sse": "^2.6.3",
52
+ "graphql-ws": "^6.4.1"
53
+ },
54
+ "peerDependenciesMeta": {
55
+ "graphql-sse": {
56
+ "optional": true
57
+ },
58
+ "graphql-ws": {
59
+ "optional": true
60
+ }
61
+ }
62
+ }