nitro-graphql 1.6.0 → 1.7.0-beta.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/dist/ecosystem/nuxt.mjs +3 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +23 -1
- package/dist/rollup.mjs +2 -2
- package/dist/routes/apollo-server-ws.d.mts +6 -0
- package/dist/routes/apollo-server-ws.mjs +298 -0
- package/dist/routes/apollo-server.d.mts +2 -2
- package/dist/routes/apollo-server.mjs +2 -2
- package/dist/routes/graphql-yoga-ws.d.mts +6 -0
- package/dist/routes/graphql-yoga-ws.mjs +298 -0
- package/dist/routes/graphql-yoga.d.mts +2 -2
- package/dist/subscribe/index.d.mts +146 -0
- package/dist/subscribe/index.mjs +830 -0
- package/dist/templates/subscribe-client.mjs +59 -0
- package/dist/types/index.d.mts +16 -1
- package/dist/utils/apollo.d.mts +1 -1
- package/dist/utils/apollo.mjs +1 -1
- package/dist/utils/client-codegen.d.mts +15 -1
- package/dist/utils/client-codegen.mjs +407 -8
- package/dist/utils/define.d.mts +1 -1
- package/dist/utils/type-generation.mjs +7 -3
- package/dist/utils/ws-protocol.d.mts +25 -0
- package/dist/utils/ws-protocol.mjs +99 -0
- package/dist/utils/ws-schema.d.mts +6 -0
- package/dist/utils/ws-schema.mjs +58 -0
- package/package.json +59 -56
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//#region src/templates/subscribe-client.ts
|
|
2
|
+
/**
|
|
3
|
+
* Template for GraphQL subscription client config
|
|
4
|
+
* Small config file that users can customize (like ofetch.ts pattern)
|
|
5
|
+
*/
|
|
6
|
+
const subscribeClientTemplate = `// This file is auto-generated once by nitro-graphql for quick start
|
|
7
|
+
// You can modify this file to customize the subscription client
|
|
8
|
+
// The implementation comes from nitro-graphql/subscribe - you'll get updates automatically!
|
|
9
|
+
//
|
|
10
|
+
// Usage Examples:
|
|
11
|
+
// ---------------
|
|
12
|
+
// Basic subscription:
|
|
13
|
+
// subscriptionClient.subscribe(query, variables, onData, onError)
|
|
14
|
+
//
|
|
15
|
+
// Multiplexed subscriptions (shared connection):
|
|
16
|
+
// const session = subscriptionClient.createSession()
|
|
17
|
+
// session.subscribe(query1, vars1, onData1)
|
|
18
|
+
// session.subscribe(query2, vars2, onData2)
|
|
19
|
+
//
|
|
20
|
+
// With authentication:
|
|
21
|
+
// Edit connectionParams below to add auth headers
|
|
22
|
+
|
|
23
|
+
import type {
|
|
24
|
+
ConnectionState,
|
|
25
|
+
SubscriptionClient,
|
|
26
|
+
SubscriptionClientConfig,
|
|
27
|
+
SubscriptionHandle,
|
|
28
|
+
SubscriptionSession,
|
|
29
|
+
} from 'nitro-graphql/subscribe'
|
|
30
|
+
import { createSubscriptionClient } from 'nitro-graphql/subscribe'
|
|
31
|
+
|
|
32
|
+
// Re-export types for convenience
|
|
33
|
+
export type { ConnectionState, SubscriptionClient, SubscriptionHandle, SubscriptionSession }
|
|
34
|
+
|
|
35
|
+
// Configure the subscription client
|
|
36
|
+
// Customize these settings according to your needs
|
|
37
|
+
const config: SubscriptionClientConfig = {
|
|
38
|
+
// WebSocket endpoint (default: '/api/graphql/ws')
|
|
39
|
+
wsEndpoint: '/api/graphql/ws',
|
|
40
|
+
|
|
41
|
+
// Connection timeout in ms (default: 10000)
|
|
42
|
+
connectionTimeoutMs: 10000,
|
|
43
|
+
|
|
44
|
+
// Max reconnection attempts (default: 5)
|
|
45
|
+
maxRetries: 5,
|
|
46
|
+
|
|
47
|
+
// Authentication params sent with connection_init
|
|
48
|
+
// Can be a function for dynamic values (e.g., JWT tokens)
|
|
49
|
+
// connectionParams: () => ({
|
|
50
|
+
// authorization: \`Bearer \${getToken()}\`,
|
|
51
|
+
// }),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Export configured client instance
|
|
55
|
+
export const subscriptionClient = createSubscriptionClient(config)
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
//#endregion
|
|
59
|
+
export { subscribeClientTemplate };
|
package/dist/types/index.d.mts
CHANGED
|
@@ -187,12 +187,27 @@ interface PathsConfig {
|
|
|
187
187
|
/** Types directory (default: '{buildDir}/types') */
|
|
188
188
|
typesDir?: string;
|
|
189
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* WebSocket subscriptions configuration
|
|
192
|
+
*/
|
|
193
|
+
interface SubscriptionsConfig {
|
|
194
|
+
/** Enable WebSocket subscriptions support */
|
|
195
|
+
enabled?: boolean;
|
|
196
|
+
/** WebSocket endpoint path (default: '/api/graphql/ws') */
|
|
197
|
+
endpoint?: string;
|
|
198
|
+
/** WebSocket protocol (currently only graphql-ws is supported) */
|
|
199
|
+
protocol?: 'graphql-ws';
|
|
200
|
+
}
|
|
190
201
|
interface NitroGraphQLOptions {
|
|
191
202
|
framework: 'graphql-yoga' | 'apollo-server';
|
|
192
203
|
endpoint?: {
|
|
193
204
|
graphql?: string;
|
|
194
205
|
healthCheck?: string;
|
|
206
|
+
/** WebSocket endpoint path (default: '/api/graphql/ws') */
|
|
207
|
+
ws?: string;
|
|
195
208
|
};
|
|
209
|
+
/** WebSocket subscriptions configuration */
|
|
210
|
+
subscriptions?: SubscriptionsConfig;
|
|
196
211
|
playground?: boolean;
|
|
197
212
|
typedefs?: string[];
|
|
198
213
|
resolvers?: Array<IResolvers<any, any>>;
|
|
@@ -243,4 +258,4 @@ interface NitroGraphQLOptions {
|
|
|
243
258
|
paths?: PathsConfig;
|
|
244
259
|
}
|
|
245
260
|
//#endregion
|
|
246
|
-
export { ClientUtilsConfig, CodegenClientConfig, CodegenServerConfig, ExternalGraphQLService, ExternalServicePaths, FederationConfig, FileGenerationConfig, GenImport, GenericSdkConfig, NitroGraphQLOptions, PathsConfig, ScaffoldConfig, SdkConfig, TypesConfig };
|
|
261
|
+
export { ClientUtilsConfig, CodegenClientConfig, CodegenServerConfig, ExternalGraphQLService, ExternalServicePaths, FederationConfig, FileGenerationConfig, GenImport, GenericSdkConfig, NitroGraphQLOptions, PathsConfig, ScaffoldConfig, SdkConfig, SubscriptionsConfig, TypesConfig };
|
package/dist/utils/apollo.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ApolloServer, BaseContext, ContextFunction } from "@apollo/server";
|
|
2
1
|
import { EventHandler, H3Event } from "h3";
|
|
2
|
+
import { ApolloServer, BaseContext, ContextFunction } from "@apollo/server";
|
|
3
3
|
import { WithRequired } from "@apollo/utils.withrequired";
|
|
4
4
|
import { Hooks } from "crossws";
|
|
5
5
|
|
package/dist/utils/apollo.mjs
CHANGED
|
@@ -34,5 +34,19 @@ declare function generateExternalClientTypes(service: ExternalGraphQLService, sc
|
|
|
34
34
|
types: string;
|
|
35
35
|
sdk: string;
|
|
36
36
|
} | false>;
|
|
37
|
+
/**
|
|
38
|
+
* Extract subscription info from documents
|
|
39
|
+
*/
|
|
40
|
+
interface SubscriptionInfo {
|
|
41
|
+
name: string;
|
|
42
|
+
methodName: string;
|
|
43
|
+
fieldName: string;
|
|
44
|
+
hasVariables: boolean;
|
|
45
|
+
}
|
|
46
|
+
declare function extractSubscriptions(docs: Source[]): SubscriptionInfo[];
|
|
47
|
+
/**
|
|
48
|
+
* Generate subscription builder code (Drizzle-style API) + Vue Composables
|
|
49
|
+
*/
|
|
50
|
+
declare function generateSubscriptionBuilder(docs: Source[]): string;
|
|
37
51
|
//#endregion
|
|
38
|
-
export { GraphQLLoadSchemaOptions, GraphQLTypeDefPointer, downloadAndSaveSchema, generateClientTypes, generateExternalClientTypes, graphQLLoadSchemaSync, loadExternalSchema, loadGraphQLDocuments };
|
|
52
|
+
export { GraphQLLoadSchemaOptions, GraphQLTypeDefPointer, SubscriptionInfo, downloadAndSaveSchema, extractSubscriptions, generateClientTypes, generateExternalClientTypes, generateSubscriptionBuilder, graphQLLoadSchemaSync, loadExternalSchema, loadGraphQLDocuments };
|
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { consola as consola$1 } from "consola";
|
|
3
3
|
import { defu as defu$1 } from "defu";
|
|
4
4
|
import { dirname, resolve } from "pathe";
|
|
5
|
-
import { parse } from "graphql";
|
|
5
|
+
import { Kind, parse } from "graphql";
|
|
6
6
|
import { printSchemaWithDirectives } from "@graphql-tools/utils";
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
import { codegen } from "@graphql-codegen/core";
|
|
@@ -265,14 +265,16 @@ export function getSdk(requester: Requester): Sdk {
|
|
|
265
265
|
typescriptGenericSdk: { plugin: plugin$1 }
|
|
266
266
|
}
|
|
267
267
|
});
|
|
268
|
+
const sdkContent = (await Promise.all(sdkOutput.map(async (config$1) => {
|
|
269
|
+
return {
|
|
270
|
+
file: config$1.filename,
|
|
271
|
+
content: await codegen(config$1)
|
|
272
|
+
};
|
|
273
|
+
})))[0]?.content || "";
|
|
274
|
+
const subscriptionBuilder = generateSubscriptionBuilder(docs);
|
|
268
275
|
return {
|
|
269
276
|
types: output,
|
|
270
|
-
sdk:
|
|
271
|
-
return {
|
|
272
|
-
file: config$1.filename,
|
|
273
|
-
content: await codegen(config$1)
|
|
274
|
-
};
|
|
275
|
-
})))[0]?.content || ""
|
|
277
|
+
sdk: subscriptionBuilder ? sdkContent + subscriptionBuilder : sdkContent
|
|
276
278
|
};
|
|
277
279
|
} catch (error) {
|
|
278
280
|
consola$1.warn(`[graphql${serviceLabel}] Client type generation failed:`, error);
|
|
@@ -285,6 +287,403 @@ export function getSdk(requester: Requester): Sdk {
|
|
|
285
287
|
async function generateExternalClientTypes(service, schema, docs, virtualTypesPath) {
|
|
286
288
|
return generateClientTypes(schema, docs, service.codegen?.client || {}, service.codegen?.clientSDK || {}, void 0, service.name, virtualTypesPath);
|
|
287
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Convert PascalCase to camelCase
|
|
292
|
+
*/
|
|
293
|
+
function toCamelCase(str) {
|
|
294
|
+
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
295
|
+
}
|
|
296
|
+
function extractSubscriptions(docs) {
|
|
297
|
+
const subscriptions = [];
|
|
298
|
+
for (const doc of docs) {
|
|
299
|
+
if (!doc.document) continue;
|
|
300
|
+
for (const def of doc.document.definitions) if (def.kind === Kind.OPERATION_DEFINITION && def.operation === "subscription") {
|
|
301
|
+
const operationDef = def;
|
|
302
|
+
const name = operationDef.name?.value;
|
|
303
|
+
if (!name) continue;
|
|
304
|
+
const firstSelection = operationDef.selectionSet.selections[0];
|
|
305
|
+
if (firstSelection.kind !== Kind.FIELD) continue;
|
|
306
|
+
const fieldName = firstSelection.name.value;
|
|
307
|
+
const hasVariables = (operationDef.variableDefinitions?.length || 0) > 0;
|
|
308
|
+
subscriptions.push({
|
|
309
|
+
name,
|
|
310
|
+
methodName: toCamelCase(name),
|
|
311
|
+
fieldName,
|
|
312
|
+
hasVariables
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return subscriptions;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Generate subscription builder code (Drizzle-style API) + Vue Composables
|
|
320
|
+
*/
|
|
321
|
+
function generateSubscriptionBuilder(docs) {
|
|
322
|
+
const subscriptions = extractSubscriptions(docs);
|
|
323
|
+
if (subscriptions.length === 0) return "";
|
|
324
|
+
let output = `
|
|
325
|
+
// === Subscription Imports ===
|
|
326
|
+
import { ref, onUnmounted, computed } from 'vue'
|
|
327
|
+
import type { Ref } from 'vue'
|
|
328
|
+
import type {
|
|
329
|
+
ConnectionState,
|
|
330
|
+
SubscriptionHandle,
|
|
331
|
+
SubscriptionSession,
|
|
332
|
+
SubscriptionTransport,
|
|
333
|
+
TransportOptions,
|
|
334
|
+
} from 'nitro-graphql/subscribe'
|
|
335
|
+
import { subscriptionClient } from './subscribe'
|
|
336
|
+
|
|
337
|
+
// === Subscription Types ===
|
|
338
|
+
export type { ConnectionState, SubscriptionHandle, SubscriptionSession, SubscriptionTransport, TransportOptions }
|
|
339
|
+
|
|
340
|
+
// Forward declaration for UseSubscriptionSessionReturn (defined below)
|
|
341
|
+
export interface UseSubscriptionSessionReturn {
|
|
342
|
+
/** The underlying session object */
|
|
343
|
+
session: SubscriptionSession
|
|
344
|
+
/** Subscribe using the shared session (updates reactive refs) */
|
|
345
|
+
subscribe: <TData = unknown>(
|
|
346
|
+
query: string,
|
|
347
|
+
variables: unknown,
|
|
348
|
+
onData?: (data: TData) => void,
|
|
349
|
+
onError?: (error: Error) => void,
|
|
350
|
+
) => SubscriptionHandle
|
|
351
|
+
/** Close all subscriptions and the connection */
|
|
352
|
+
close: () => void
|
|
353
|
+
/** Is the session connected (reactive) */
|
|
354
|
+
isConnected: Ref<boolean>
|
|
355
|
+
/** Current connection state (reactive) */
|
|
356
|
+
state: Ref<ConnectionState>
|
|
357
|
+
/** Number of active subscriptions (reactive) */
|
|
358
|
+
subscriptionCount: Ref<number>
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export interface UseSubscriptionOptions<T> {
|
|
362
|
+
/** Auto-start subscription on mount (default: false) */
|
|
363
|
+
immediate?: boolean
|
|
364
|
+
/** Callback when subscription starts */
|
|
365
|
+
onStart?: () => void
|
|
366
|
+
/** Callback when subscription stops */
|
|
367
|
+
onStop?: () => void
|
|
368
|
+
/** Callback when data is received */
|
|
369
|
+
onData?: (data: T) => void
|
|
370
|
+
/** Callback when error occurs */
|
|
371
|
+
onError?: (error: Error) => void
|
|
372
|
+
/** Callback when WebSocket connects */
|
|
373
|
+
onConnected?: () => void
|
|
374
|
+
/** Callback when WebSocket reconnects */
|
|
375
|
+
onReconnected?: () => void
|
|
376
|
+
/** Callback when WebSocket disconnects */
|
|
377
|
+
onDisconnected?: () => void
|
|
378
|
+
/** Callback when connection state changes */
|
|
379
|
+
onStateChange?: (state: ConnectionState) => void
|
|
380
|
+
/** Use existing session for multiplexing (pass result from useSubscriptionSession) */
|
|
381
|
+
session?: UseSubscriptionSessionReturn
|
|
382
|
+
/** Use SSE transport instead of WebSocket (shorthand for transport: 'sse') */
|
|
383
|
+
sse?: boolean
|
|
384
|
+
/** Transport type: 'websocket' (default), 'sse', or 'auto' (WS first, SSE fallback) */
|
|
385
|
+
transport?: SubscriptionTransport
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export interface UseSubscriptionReturn<T> {
|
|
389
|
+
/** Reactive subscription data */
|
|
390
|
+
data: Ref<T | null>
|
|
391
|
+
/** Reactive error state */
|
|
392
|
+
error: Ref<Error | null>
|
|
393
|
+
/** Is subscription active */
|
|
394
|
+
isActive: Ref<boolean>
|
|
395
|
+
/** Connection state */
|
|
396
|
+
state: Ref<ConnectionState>
|
|
397
|
+
/** Active transport type ('websocket' | 'sse') */
|
|
398
|
+
transport: Ref<'websocket' | 'sse'>
|
|
399
|
+
/** Start subscription */
|
|
400
|
+
start: () => void
|
|
401
|
+
/** Stop subscription */
|
|
402
|
+
stop: () => void
|
|
403
|
+
/** Restart subscription */
|
|
404
|
+
restart: () => void
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// === Subscription Builder (Drizzle-style API) ===
|
|
408
|
+
interface SubscriptionBuilder<TData> {
|
|
409
|
+
onData(fn: (data: TData) => void): SubscriptionBuilder<TData>
|
|
410
|
+
onError(fn: (error: Error) => void): SubscriptionBuilder<TData>
|
|
411
|
+
start(): SubscriptionHandle
|
|
412
|
+
subscribe(fn: (data: TData) => void): SubscriptionHandle
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function createSubscriptionBuilder<TData>(query: string, variables: unknown): SubscriptionBuilder<TData> {
|
|
416
|
+
let onDataFn: ((data: TData) => void) | undefined
|
|
417
|
+
let onErrorFn: ((error: Error) => void) | undefined
|
|
418
|
+
|
|
419
|
+
const builder: SubscriptionBuilder<TData> = {
|
|
420
|
+
onData(fn: (data: TData) => void) {
|
|
421
|
+
onDataFn = fn
|
|
422
|
+
return builder
|
|
423
|
+
},
|
|
424
|
+
onError(fn: (error: Error) => void) {
|
|
425
|
+
onErrorFn = fn
|
|
426
|
+
return builder
|
|
427
|
+
},
|
|
428
|
+
start(): SubscriptionHandle {
|
|
429
|
+
return subscriptionClient.subscribe(query, variables, onDataFn, onErrorFn)
|
|
430
|
+
},
|
|
431
|
+
subscribe(fn: (data: TData) => void): SubscriptionHandle {
|
|
432
|
+
return subscriptionClient.subscribe(query, variables, fn, undefined)
|
|
433
|
+
},
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return builder
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export const subscription = {
|
|
440
|
+
`;
|
|
441
|
+
for (const sub of subscriptions) if (sub.hasVariables) output += ` ${sub.methodName}(variables: Types.${sub.name}SubscriptionVariables): SubscriptionBuilder<Types.${sub.name}Subscription['${sub.fieldName}']> {
|
|
442
|
+
return createSubscriptionBuilder<Types.${sub.name}Subscription['${sub.fieldName}']>(${sub.name}Document, variables)
|
|
443
|
+
},
|
|
444
|
+
`;
|
|
445
|
+
else output += ` ${sub.methodName}(): SubscriptionBuilder<Types.${sub.name}Subscription['${sub.fieldName}']> {
|
|
446
|
+
return createSubscriptionBuilder<Types.${sub.name}Subscription['${sub.fieldName}']>(${sub.name}Document, undefined)
|
|
447
|
+
},
|
|
448
|
+
`;
|
|
449
|
+
output += `}
|
|
450
|
+
|
|
451
|
+
// === Framework-Agnostic Session (for non-Vue usage) ===
|
|
452
|
+
/**
|
|
453
|
+
* Create a multiplexed subscription session (framework-agnostic)
|
|
454
|
+
* All subscriptions share a single WebSocket connection.
|
|
455
|
+
*
|
|
456
|
+
* @example
|
|
457
|
+
* // Vanilla JS / Node.js / React / etc.
|
|
458
|
+
* const session = createSubscriptionSession()
|
|
459
|
+
* const sub1 = session.subscribe(query1, vars1, onData1)
|
|
460
|
+
* const sub2 = session.subscribe(query2, vars2, onData2)
|
|
461
|
+
* // Both use the same WebSocket connection
|
|
462
|
+
* sub1.unsubscribe()
|
|
463
|
+
* session.close() // Close all
|
|
464
|
+
*
|
|
465
|
+
* @returns SubscriptionSession - Framework-agnostic session object
|
|
466
|
+
*/
|
|
467
|
+
export function createSubscriptionSession(): SubscriptionSession {
|
|
468
|
+
return subscriptionClient.createSession()
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// === Vue Composable: useSubscriptionSession (Multiplexing) ===
|
|
472
|
+
export interface UseSubscriptionSessionReturn {
|
|
473
|
+
/** The underlying session object */
|
|
474
|
+
session: SubscriptionSession
|
|
475
|
+
/** Subscribe using the shared session */
|
|
476
|
+
subscribe: <TData = unknown>(
|
|
477
|
+
query: string,
|
|
478
|
+
variables: unknown,
|
|
479
|
+
onData?: (data: TData) => void,
|
|
480
|
+
onError?: (error: Error) => void,
|
|
481
|
+
) => SubscriptionHandle
|
|
482
|
+
/** Close all subscriptions and the connection */
|
|
483
|
+
close: () => void
|
|
484
|
+
/** Is the session connected (reactive) */
|
|
485
|
+
isConnected: Ref<boolean>
|
|
486
|
+
/** Current connection state (reactive) */
|
|
487
|
+
state: Ref<ConnectionState>
|
|
488
|
+
/** Number of active subscriptions (reactive) */
|
|
489
|
+
subscriptionCount: Ref<number>
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Vue composable for multiplexed subscription session
|
|
494
|
+
* Provides reactive state and automatic cleanup on unmount.
|
|
495
|
+
*
|
|
496
|
+
* @example
|
|
497
|
+
* // Vue 3 component
|
|
498
|
+
* const session = useSubscriptionSession()
|
|
499
|
+
* const { data } = useCountdown({ from: 10 }, { session })
|
|
500
|
+
* // Session auto-closes on component unmount
|
|
501
|
+
*
|
|
502
|
+
* @returns UseSubscriptionSessionReturn - Vue-reactive session wrapper
|
|
503
|
+
*/
|
|
504
|
+
export function useSubscriptionSession(): UseSubscriptionSessionReturn {
|
|
505
|
+
const session = subscriptionClient.createSession()
|
|
506
|
+
|
|
507
|
+
// Use refs for reactivity (session getters are not reactive)
|
|
508
|
+
const isConnected = ref(session.isConnected)
|
|
509
|
+
const state = ref<ConnectionState>(session.state)
|
|
510
|
+
const subscriptionCount = ref(session.subscriptionCount)
|
|
511
|
+
|
|
512
|
+
// Update refs when session state changes
|
|
513
|
+
function updateRefs() {
|
|
514
|
+
isConnected.value = session.isConnected
|
|
515
|
+
state.value = session.state
|
|
516
|
+
subscriptionCount.value = session.subscriptionCount
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Subscribe to session state changes for automatic reactivity
|
|
520
|
+
const unsubscribeStateChange = session.onStateChange(() => {
|
|
521
|
+
updateRefs()
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
function subscribe<TData = unknown>(
|
|
525
|
+
query: string,
|
|
526
|
+
variables: unknown,
|
|
527
|
+
onData?: (data: TData) => void,
|
|
528
|
+
onError?: (error: Error) => void,
|
|
529
|
+
): SubscriptionHandle {
|
|
530
|
+
return session.subscribe(query, variables, onData as (data: unknown) => void, onError)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function close() {
|
|
534
|
+
session.close()
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
onUnmounted(() => {
|
|
538
|
+
unsubscribeStateChange()
|
|
539
|
+
close()
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
return {
|
|
543
|
+
session,
|
|
544
|
+
subscribe,
|
|
545
|
+
close,
|
|
546
|
+
isConnected,
|
|
547
|
+
state,
|
|
548
|
+
subscriptionCount,
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// === Vue Composables ===
|
|
553
|
+
function createUseSubscription<TData, TVariables = undefined>(
|
|
554
|
+
query: string,
|
|
555
|
+
getVariables: () => TVariables,
|
|
556
|
+
): (options?: UseSubscriptionOptions<TData>) => UseSubscriptionReturn<TData> {
|
|
557
|
+
return (options: UseSubscriptionOptions<TData> = {}): UseSubscriptionReturn<TData> => {
|
|
558
|
+
const data = ref<TData | null>(null) as Ref<TData | null>
|
|
559
|
+
const error = ref<Error | null>(null)
|
|
560
|
+
const isActive = ref(false)
|
|
561
|
+
const state = ref<ConnectionState>('idle')
|
|
562
|
+
const transport = ref<'websocket' | 'sse'>('websocket')
|
|
563
|
+
let handle: SubscriptionHandle | null = null
|
|
564
|
+
|
|
565
|
+
// Resolve transport options
|
|
566
|
+
const transportOptions: TransportOptions = {
|
|
567
|
+
sse: options.sse,
|
|
568
|
+
transport: options.transport,
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function start() {
|
|
572
|
+
stop()
|
|
573
|
+
isActive.value = true
|
|
574
|
+
error.value = null
|
|
575
|
+
options.onStart?.()
|
|
576
|
+
|
|
577
|
+
const variables = getVariables()
|
|
578
|
+
|
|
579
|
+
if (options.session) {
|
|
580
|
+
// Use existing session for multiplexing (WebSocket only)
|
|
581
|
+
handle = options.session.subscribe<TData>(
|
|
582
|
+
query,
|
|
583
|
+
variables,
|
|
584
|
+
(d: TData) => {
|
|
585
|
+
data.value = d
|
|
586
|
+
options.onData?.(d)
|
|
587
|
+
},
|
|
588
|
+
(e: Error) => {
|
|
589
|
+
error.value = e
|
|
590
|
+
options.onError?.(e)
|
|
591
|
+
},
|
|
592
|
+
)
|
|
593
|
+
transport.value = 'websocket'
|
|
594
|
+
} else {
|
|
595
|
+
// Create dedicated connection with transport selection
|
|
596
|
+
handle = subscriptionClient.subscribe<TData>(
|
|
597
|
+
query,
|
|
598
|
+
variables,
|
|
599
|
+
(d: TData) => {
|
|
600
|
+
data.value = d
|
|
601
|
+
options.onData?.(d)
|
|
602
|
+
},
|
|
603
|
+
(e: Error) => {
|
|
604
|
+
error.value = e
|
|
605
|
+
options.onError?.(e)
|
|
606
|
+
},
|
|
607
|
+
transportOptions,
|
|
608
|
+
)
|
|
609
|
+
// Update transport ref from handle
|
|
610
|
+
transport.value = handle.transport
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function stop() {
|
|
615
|
+
if (handle) {
|
|
616
|
+
handle.unsubscribe()
|
|
617
|
+
handle = null
|
|
618
|
+
isActive.value = false
|
|
619
|
+
options.onStop?.()
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function restart() {
|
|
624
|
+
stop()
|
|
625
|
+
start()
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (options.immediate) {
|
|
629
|
+
start()
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
onUnmounted(stop)
|
|
633
|
+
|
|
634
|
+
return { data, error, isActive, state, transport, start, stop, restart }
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// === Subscription Return Types ===
|
|
639
|
+
`;
|
|
640
|
+
for (const sub of subscriptions) {
|
|
641
|
+
const typeName = `Types.${sub.name}Subscription['${sub.fieldName}']`;
|
|
642
|
+
output += `/** Return type for use${sub.name} composable */
|
|
643
|
+
export type Use${sub.name}Return = UseSubscriptionReturn<${typeName}>
|
|
644
|
+
`;
|
|
645
|
+
}
|
|
646
|
+
output += `
|
|
647
|
+
// === Vue Composables ===
|
|
648
|
+
`;
|
|
649
|
+
for (const sub of subscriptions) {
|
|
650
|
+
const typeName = `Types.${sub.name}Subscription['${sub.fieldName}']`;
|
|
651
|
+
const varsType = `Types.${sub.name}SubscriptionVariables`;
|
|
652
|
+
if (sub.hasVariables) output += `/**
|
|
653
|
+
* Vue composable for ${sub.name} subscription
|
|
654
|
+
* @param variables - Subscription variables
|
|
655
|
+
* @param options - Subscription options (immediate, onData, onError, session, etc.)
|
|
656
|
+
* @returns Reactive subscription state: { data, error, isActive, state, start, stop, restart }
|
|
657
|
+
*/
|
|
658
|
+
export function use${sub.name}(
|
|
659
|
+
variables: ${varsType},
|
|
660
|
+
options?: UseSubscriptionOptions<${typeName}>,
|
|
661
|
+
): Use${sub.name}Return {
|
|
662
|
+
return createUseSubscription<${typeName}, ${varsType}>(
|
|
663
|
+
${sub.name}Document,
|
|
664
|
+
() => variables,
|
|
665
|
+
)(options)
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
`;
|
|
669
|
+
else output += `/**
|
|
670
|
+
* Vue composable for ${sub.name} subscription
|
|
671
|
+
* @param options - Subscription options (immediate, onData, onError, session, etc.)
|
|
672
|
+
* @returns Reactive subscription state: { data, error, isActive, state, start, stop, restart }
|
|
673
|
+
*/
|
|
674
|
+
export function use${sub.name}(
|
|
675
|
+
options?: UseSubscriptionOptions<${typeName}>,
|
|
676
|
+
): Use${sub.name}Return {
|
|
677
|
+
return createUseSubscription<${typeName}, undefined>(
|
|
678
|
+
${sub.name}Document,
|
|
679
|
+
() => undefined,
|
|
680
|
+
)(options)
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
`;
|
|
684
|
+
}
|
|
685
|
+
return output;
|
|
686
|
+
}
|
|
288
687
|
|
|
289
688
|
//#endregion
|
|
290
|
-
export { downloadAndSaveSchema, generateClientTypes, generateExternalClientTypes, graphQLLoadSchemaSync, loadExternalSchema, loadGraphQLDocuments };
|
|
689
|
+
export { downloadAndSaveSchema, extractSubscriptions, generateClientTypes, generateExternalClientTypes, generateSubscriptionBuilder, graphQLLoadSchemaSync, loadExternalSchema, loadGraphQLDocuments };
|
package/dist/utils/define.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { GraphQLSchema } from "graphql";
|
|
2
|
-
import { ApolloServerOptions } from "@apollo/server";
|
|
3
2
|
import { H3Event } from "h3";
|
|
3
|
+
import { ApolloServerOptions } from "@apollo/server";
|
|
4
4
|
import { YogaServerOptions } from "graphql-yoga";
|
|
5
5
|
import { NPMConfig, Resolvers, ResolversTypes } from "#graphql/server";
|
|
6
6
|
import { StandardSchemaV1 } from "nitro-graphql";
|
|
@@ -22,11 +22,15 @@ function generateGraphQLIndexFile(nitro, clientDir, externalServices = []) {
|
|
|
22
22
|
let indexContent = `// This file is auto-generated once by nitro-graphql for quick start
|
|
23
23
|
// You can modify this file according to your needs
|
|
24
24
|
//
|
|
25
|
-
//
|
|
25
|
+
// === Query/Mutation Client ===
|
|
26
26
|
export * from './default/ofetch'
|
|
27
27
|
|
|
28
|
-
//
|
|
29
|
-
//
|
|
28
|
+
// === Subscription Composables (Vue) ===
|
|
29
|
+
// Export useCountdown, useGreetings, useSubscriptionSession, etc.
|
|
30
|
+
export * from './default/sdk'
|
|
31
|
+
|
|
32
|
+
// === External GraphQL Services ===
|
|
33
|
+
// When you add new external services, add their exports here:
|
|
30
34
|
// export * from './yourServiceName/ofetch'
|
|
31
35
|
`;
|
|
32
36
|
for (const service of externalServices) indexContent += `export * from './${service.name}/ofetch'\n`;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { GraphQLSchema } from "graphql";
|
|
2
|
+
import { Peer } from "crossws";
|
|
3
|
+
|
|
4
|
+
//#region src/utils/ws-protocol.d.ts
|
|
5
|
+
declare function devLog(message: string, ...args: any[]): void;
|
|
6
|
+
interface GraphQLWSMessage {
|
|
7
|
+
id?: string;
|
|
8
|
+
type: string;
|
|
9
|
+
payload?: any;
|
|
10
|
+
}
|
|
11
|
+
declare function sendMessage(peer: Peer, message: Record<string, any>): void;
|
|
12
|
+
declare function sendErrorMessage(peer: Peer, id: string | undefined, errors: Array<{
|
|
13
|
+
message: string;
|
|
14
|
+
locations?: any;
|
|
15
|
+
path?: any;
|
|
16
|
+
}>): void;
|
|
17
|
+
declare function sendNextMessage(peer: Peer, id: string, payload: any): void;
|
|
18
|
+
declare function sendCompleteMessage(peer: Peer, id: string): void;
|
|
19
|
+
declare function handleConnectionInit(peer: Peer): void;
|
|
20
|
+
declare function handlePing(peer: Peer): void;
|
|
21
|
+
declare function handleSubscribe(peer: Peer, msg: GraphQLWSMessage, schema: GraphQLSchema, subscriptions: Map<string, AsyncIterator<any>>): Promise<void>;
|
|
22
|
+
declare function handleComplete(msg: GraphQLWSMessage, subscriptions: Map<string, AsyncIterator<any>>): Promise<void>;
|
|
23
|
+
declare function cleanupSubscriptions(subscriptions: Map<string, AsyncIterator<any>>): Promise<void>;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { GraphQLWSMessage, cleanupSubscriptions, devLog, handleComplete, handleConnectionInit, handlePing, handleSubscribe, sendCompleteMessage, sendErrorMessage, sendMessage, sendNextMessage };
|