@constructive-io/graphql-codegen 4.38.2 → 4.39.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,12 +15,36 @@ import type {
15
15
  } from '@constructive-io/graphql-query/runtime';
16
16
  import { createFetch } from '@constructive-io/graphql-query/runtime';
17
17
 
18
+ import type {
19
+ ConnectionState,
20
+ ConnectionStateListener,
21
+ RealtimeConfig,
22
+ SubscribeOptions,
23
+ SubscriptionEvent,
24
+ SubscriptionFieldMeta,
25
+ Unsubscribe,
26
+ } from './realtime';
27
+ import { RealtimeManager } from './realtime';
28
+
18
29
  export type {
19
30
  GraphQLAdapter,
20
31
  GraphQLError,
21
32
  QueryResult,
22
33
  } from '@constructive-io/graphql-query/runtime';
23
34
 
35
+ export type {
36
+ ConnectionState,
37
+ ConnectionStateListener,
38
+ RealtimeConfig,
39
+ SubscribeOptions,
40
+ SubscriptionEvent,
41
+ SubscriptionFieldMeta,
42
+ SubscriptionOperation,
43
+ Unsubscribe,
44
+ WsClient,
45
+ } from './realtime';
46
+ export { RealtimeManager } from './realtime';
47
+
24
48
  /**
25
49
  * Default adapter that uses fetch for HTTP requests.
26
50
  *
@@ -117,6 +141,12 @@ export interface OrmClientConfig {
117
141
  fetch?: typeof globalThis.fetch;
118
142
  /** Custom adapter for GraphQL execution (overrides endpoint/headers/fetch) */
119
143
  adapter?: GraphQLAdapter;
144
+ /**
145
+ * Optional realtime (WebSocket) configuration.
146
+ * When provided, enables subscription methods on models.
147
+ * The WebSocket connection is created lazily on first subscribe().
148
+ */
149
+ realtime?: RealtimeConfig;
120
150
  }
121
151
 
122
152
  /**
@@ -135,6 +165,7 @@ export class GraphQLRequestError extends Error {
135
165
 
136
166
  export class OrmClient {
137
167
  private adapter: GraphQLAdapter;
168
+ private realtimeManager?: RealtimeManager;
138
169
 
139
170
  constructor(config: OrmClientConfig) {
140
171
  if (config.adapter) {
@@ -150,6 +181,10 @@ export class OrmClient {
150
181
  'OrmClientConfig requires either an endpoint or a custom adapter',
151
182
  );
152
183
  }
184
+
185
+ if (config.realtime) {
186
+ this.realtimeManager = new RealtimeManager(config.realtime);
187
+ }
153
188
  }
154
189
 
155
190
  async execute<T>(
@@ -159,6 +194,34 @@ export class OrmClient {
159
194
  return this.adapter.execute<T>(document, variables);
160
195
  }
161
196
 
197
+ /**
198
+ * Subscribe to a GraphQL subscription operation.
199
+ * Used by generated model subscribe() methods.
200
+ * @throws Error if realtime is not configured
201
+ */
202
+ subscribe<T>(
203
+ meta: SubscriptionFieldMeta,
204
+ document: string,
205
+ variables: Record<string, unknown>,
206
+ options: {
207
+ onEvent: (event: SubscriptionEvent<T>) => void;
208
+ onError?: (error: Error) => void;
209
+ onComplete?: () => void;
210
+ },
211
+ ): Unsubscribe {
212
+ if (!this.realtimeManager) {
213
+ throw new Error(
214
+ 'Realtime not configured. Pass a `realtime` option to createClient() to enable subscriptions.',
215
+ );
216
+ }
217
+ return this.realtimeManager.subscribe<T>(
218
+ meta,
219
+ document,
220
+ variables,
221
+ options,
222
+ );
223
+ }
224
+
162
225
  /**
163
226
  * Set headers for requests.
164
227
  * Only works if the adapter supports headers.
@@ -176,4 +239,32 @@ export class OrmClient {
176
239
  getEndpoint(): string {
177
240
  return this.adapter.getEndpoint?.() ?? '';
178
241
  }
242
+
243
+ /** Get current WebSocket connection state */
244
+ getConnectionState(): ConnectionState {
245
+ return this.realtimeManager?.getConnectionState() ?? 'disconnected';
246
+ }
247
+
248
+ /** Register a listener for WebSocket connection state changes */
249
+ onConnectionStateChange(
250
+ listener: ConnectionStateListener,
251
+ ): Unsubscribe {
252
+ if (!this.realtimeManager) return () => {};
253
+ return this.realtimeManager.onConnectionStateChange(listener);
254
+ }
255
+
256
+ /** Number of active subscriptions */
257
+ getActiveSubscriptionCount(): number {
258
+ return this.realtimeManager?.getActiveSubscriptionCount() ?? 0;
259
+ }
260
+
261
+ /** Whether realtime is configured */
262
+ get isRealtimeEnabled(): boolean {
263
+ return this.realtimeManager !== undefined;
264
+ }
265
+
266
+ /** Dispose the realtime manager (close WebSocket) */
267
+ dispose(): void {
268
+ this.realtimeManager?.dispose();
269
+ }
179
270
  }
@@ -0,0 +1,267 @@
1
+ /**
2
+ * ORM Realtime - WebSocket subscription manager
3
+ *
4
+ * This is the RUNTIME code that gets copied to generated output.
5
+ * Provides the WebSocket connection manager and subscription types
6
+ * for realtime subscriptions integrated into the ORM client.
7
+ *
8
+ * NOTE: This file is read at codegen time and written to output.
9
+ * Any changes here will affect all generated ORM clients.
10
+ */
11
+
12
+ // Minimal type shims so this module compiles without graphql-ws
13
+ // installed. Consumers supply a WsClient via RealtimeConfig;
14
+ // the SDK itself never imports or requires graphql-ws.
15
+
16
+ interface WsGraphQLError {
17
+ readonly message: string;
18
+ readonly [key: string]: unknown;
19
+ }
20
+
21
+ interface WsExecutionResult<TData = Record<string, unknown>> {
22
+ data?: TData | null;
23
+ errors?: readonly WsGraphQLError[];
24
+ extensions?: Record<string, unknown>;
25
+ }
26
+
27
+ interface WsSink<T> {
28
+ next(value: T): void;
29
+ error(error: unknown): void;
30
+ complete(): void;
31
+ }
32
+
33
+ /**
34
+ * Minimal interface matching the graphql-ws Client.
35
+ * Consumers pass a concrete instance via RealtimeConfig.client.
36
+ */
37
+ export interface WsClient {
38
+ subscribe<TData = Record<string, unknown>>(
39
+ payload: { query: string; variables?: Record<string, unknown> },
40
+ sink: WsSink<WsExecutionResult<TData>>,
41
+ ): () => void;
42
+ dispose(): void;
43
+ }
44
+
45
+ // ============================================================================
46
+ // Types
47
+ // ============================================================================
48
+
49
+ /** The DML operation that triggered the subscription event */
50
+ export type SubscriptionOperation = 'INSERT' | 'UPDATE' | 'DELETE';
51
+
52
+ /** Connection state of the WebSocket */
53
+ export type ConnectionState =
54
+ | 'disconnected'
55
+ | 'connecting'
56
+ | 'connected'
57
+ | 'reconnecting';
58
+
59
+ /** Listener for connection state changes */
60
+ export type ConnectionStateListener = (state: ConnectionState) => void;
61
+
62
+ /** Function returned by subscribe() to cancel the subscription */
63
+ export type Unsubscribe = () => void;
64
+
65
+ /**
66
+ * A realtime subscription event delivered to the client.
67
+ *
68
+ * @typeParam T - The row type of the subscribed table
69
+ */
70
+ export interface SubscriptionEvent<T> {
71
+ /** The DML operation that triggered this event */
72
+ operation: SubscriptionOperation;
73
+ /** The current row data (null for DELETE if row is no longer visible) */
74
+ data: T | null;
75
+ /** Previous field values (populated on UPDATE when available) */
76
+ previousValues?: Partial<T>;
77
+ /** Server-side timestamp of when the change occurred */
78
+ timestamp: string;
79
+ }
80
+
81
+ /**
82
+ * Options for creating a subscription.
83
+ *
84
+ * @typeParam T - The row type of the subscribed table
85
+ * @typeParam TFilter - The filter type for the table
86
+ */
87
+ export interface SubscribeOptions<
88
+ T,
89
+ TFilter = Record<string, unknown>,
90
+ > {
91
+ /** Server-side filter to limit which events are delivered */
92
+ filter?: TFilter;
93
+ /** Called when a subscription event is received */
94
+ onEvent: (event: SubscriptionEvent<T>) => void;
95
+ /** Called when the subscription encounters an error */
96
+ onError?: (error: Error) => void;
97
+ /** Called when the subscription completes (server-initiated close) */
98
+ onComplete?: () => void;
99
+ }
100
+
101
+ /**
102
+ * Metadata about a subscription field, used internally to map
103
+ * table names to GraphQL subscription field names and types.
104
+ */
105
+ export interface SubscriptionFieldMeta {
106
+ /** The GraphQL subscription field name (e.g., 'onContactChanged') */
107
+ fieldName: string;
108
+ /** The table name in the source schema (e.g., 'contact') */
109
+ tableName: string;
110
+ /** The data field name inside the subscription payload (e.g., 'contact') */
111
+ dataFieldName: string;
112
+ }
113
+
114
+ /**
115
+ * Configuration for the realtime (WebSocket) connection.
116
+ * Pass this as the `realtime` option in OrmClientConfig.
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * import { createClient } from 'graphql-ws';
121
+ *
122
+ * const client = createOrmClient({
123
+ * endpoint: 'https://api.example.com/graphql',
124
+ * realtime: {
125
+ * client: createClient({ url: 'wss://api.example.com/graphql' }),
126
+ * },
127
+ * });
128
+ * ```
129
+ */
130
+ export interface RealtimeConfig {
131
+ /**
132
+ * A graphql-ws Client instance (or any object satisfying WsClient).
133
+ * The consumer creates this themselves, giving full control over
134
+ * connection options, auth, and transport.
135
+ *
136
+ * @example
137
+ * ```ts
138
+ * import { createClient } from 'graphql-ws';
139
+ * const wsClient = createClient({ url: 'wss://...' });
140
+ * ```
141
+ */
142
+ client: WsClient;
143
+ }
144
+
145
+ // ============================================================================
146
+ // RealtimeManager
147
+ // ============================================================================
148
+
149
+ /**
150
+ * Manages a graphql-ws WebSocket client and multiplexes
151
+ * subscriptions over it. Created by OrmClient when `realtime`
152
+ * config is provided.
153
+ */
154
+ export class RealtimeManager {
155
+ private wsClient: WsClient;
156
+ private connectionState: ConnectionState = 'disconnected';
157
+ private stateListeners: Set<ConnectionStateListener> = new Set();
158
+ private activeSubscriptions = 0;
159
+
160
+ constructor(config: RealtimeConfig) {
161
+ this.wsClient = config.client;
162
+ }
163
+
164
+ /**
165
+ * Subscribe to a GraphQL subscription operation.
166
+ * Models call this with typed metadata and documents.
167
+ */
168
+ subscribe<T>(
169
+ meta: SubscriptionFieldMeta,
170
+ document: string,
171
+ variables: Record<string, unknown>,
172
+ options: {
173
+ onEvent: (event: SubscriptionEvent<T>) => void;
174
+ onError?: (error: Error) => void;
175
+ onComplete?: () => void;
176
+ },
177
+ ): Unsubscribe {
178
+ this.activeSubscriptions++;
179
+ let disposed = false;
180
+
181
+ const cleanup = this.wsClient.subscribe<Record<string, unknown>>(
182
+ { query: document, variables },
183
+ {
184
+ next: (result) => {
185
+ if (disposed) return;
186
+ if (result.errors) {
187
+ options.onError?.(
188
+ new Error(
189
+ result.errors.map((e) => e.message).join('; '),
190
+ ),
191
+ );
192
+ return;
193
+ }
194
+
195
+ const payload = result.data?.[meta.fieldName] as
196
+ | { event?: string; [key: string]: unknown }
197
+ | undefined;
198
+
199
+ if (!payload) return;
200
+
201
+ const event: SubscriptionEvent<T> = {
202
+ operation:
203
+ (payload.event as SubscriptionOperation) ?? 'UPDATE',
204
+ data: (payload[meta.dataFieldName] as T) ?? null,
205
+ previousValues: payload.previousValues as
206
+ | Partial<T>
207
+ | undefined,
208
+ timestamp:
209
+ (payload.timestamp as string) ?? new Date().toISOString(),
210
+ };
211
+ options.onEvent(event);
212
+ },
213
+ error: (err) => {
214
+ if (disposed) return;
215
+ options.onError?.(
216
+ err instanceof Error ? err : new Error(String(err)),
217
+ );
218
+ },
219
+ complete: () => {
220
+ if (disposed) return;
221
+ options.onComplete?.();
222
+ },
223
+ },
224
+ );
225
+
226
+ return () => {
227
+ if (disposed) return;
228
+ disposed = true;
229
+ this.activeSubscriptions--;
230
+ cleanup();
231
+ };
232
+ }
233
+
234
+ /** Register a listener for connection state changes */
235
+ onConnectionStateChange(listener: ConnectionStateListener): Unsubscribe {
236
+ this.stateListeners.add(listener);
237
+ return () => {
238
+ this.stateListeners.delete(listener);
239
+ };
240
+ }
241
+
242
+ /** Get current connection state */
243
+ getConnectionState(): ConnectionState {
244
+ return this.connectionState;
245
+ }
246
+
247
+ /** Number of active subscriptions */
248
+ getActiveSubscriptionCount(): number {
249
+ return this.activeSubscriptions;
250
+ }
251
+
252
+ /** Dispose the manager and close the WebSocket connection */
253
+ dispose(): void {
254
+ this.wsClient.dispose();
255
+ this.stateListeners.clear();
256
+ this.activeSubscriptions = 0;
257
+ this.setConnectionState('disconnected');
258
+ }
259
+
260
+ private setConnectionState(state: ConnectionState): void {
261
+ if (this.connectionState === state) return;
262
+ this.connectionState = state;
263
+ for (const listener of this.stateListeners) {
264
+ listener(state);
265
+ }
266
+ }
267
+ }
@@ -65,6 +65,21 @@ export declare function getUpdateMutationFileName(table: Table): string;
65
65
  * Generate file name for delete mutation hook
66
66
  */
67
67
  export declare function getDeleteMutationFileName(table: Table): string;
68
+ /**
69
+ * Generate hook function name for subscription
70
+ * e.g., "useContactSubscription"
71
+ */
72
+ export declare function getSubscriptionHookName(table: Table): string;
73
+ /**
74
+ * Generate file name for subscription hook
75
+ * e.g., "useContactSubscription.ts"
76
+ */
77
+ export declare function getSubscriptionFileName(table: Table): string;
78
+ /**
79
+ * Generate the GraphQL subscription field name
80
+ * e.g., "onContactChanged"
81
+ */
82
+ export declare function getSubscriptionFieldName(table: Table): string;
68
83
  /**
69
84
  * Get the GraphQL query name for fetching all rows
70
85
  * Uses inflection from introspection, falls back to convention
@@ -12,6 +12,9 @@ exports.getSingleQueryFileName = getSingleQueryFileName;
12
12
  exports.getCreateMutationFileName = getCreateMutationFileName;
13
13
  exports.getUpdateMutationFileName = getUpdateMutationFileName;
14
14
  exports.getDeleteMutationFileName = getDeleteMutationFileName;
15
+ exports.getSubscriptionHookName = getSubscriptionHookName;
16
+ exports.getSubscriptionFileName = getSubscriptionFileName;
17
+ exports.getSubscriptionFieldName = getSubscriptionFieldName;
15
18
  exports.getAllRowsQueryName = getAllRowsQueryName;
16
19
  exports.getSingleRowQueryName = getSingleRowQueryName;
17
20
  exports.getCreateMutationName = getCreateMutationName;
@@ -138,6 +141,29 @@ function getUpdateMutationFileName(table) {
138
141
  function getDeleteMutationFileName(table) {
139
142
  return `${getDeleteMutationHookName(table)}.ts`;
140
143
  }
144
+ /**
145
+ * Generate hook function name for subscription
146
+ * e.g., "useContactSubscription"
147
+ */
148
+ function getSubscriptionHookName(table) {
149
+ const { singularName } = getTableNames(table);
150
+ return `use${(0, inflekt_1.ucFirst)(singularName)}Subscription`;
151
+ }
152
+ /**
153
+ * Generate file name for subscription hook
154
+ * e.g., "useContactSubscription.ts"
155
+ */
156
+ function getSubscriptionFileName(table) {
157
+ return `${getSubscriptionHookName(table)}.ts`;
158
+ }
159
+ /**
160
+ * Generate the GraphQL subscription field name
161
+ * e.g., "onContactChanged"
162
+ */
163
+ function getSubscriptionFieldName(table) {
164
+ const { singularName } = getTableNames(table);
165
+ return `on${(0, inflekt_1.ucFirst)(singularName)}Changed`;
166
+ }
141
167
  // ============================================================================
142
168
  // GraphQL operation names
143
169
  // ============================================================================
@@ -15,6 +15,8 @@ export declare function generateMutationsBarrel(tables: Table[]): string;
15
15
  */
16
16
  export interface MainBarrelOptions {
17
17
  hasMutations?: boolean;
18
+ /** Whether subscriptions/ directory was generated */
19
+ hasSubscriptions?: boolean;
18
20
  /** Whether query-keys.ts was generated */
19
21
  hasQueryKeys?: boolean;
20
22
  /** Whether mutation-keys.ts was generated */
@@ -44,6 +46,10 @@ export declare function generateRootBarrel(options?: RootBarrelOptions): string;
44
46
  * export * as public_ from './public';
45
47
  */
46
48
  export declare function generateMultiTargetBarrel(targetNames: string[]): string;
49
+ /**
50
+ * Generate the subscriptions/index.ts barrel file
51
+ */
52
+ export declare function generateSubscriptionsBarrel(tables: Table[]): string;
47
53
  /**
48
54
  * Generate queries barrel including custom query operations
49
55
  */
@@ -6,7 +6,7 @@
6
6
  import * as t from '@babel/types';
7
7
  import { addJSDocComment, generateCode } from './babel-ast';
8
8
  import { getOperationHookName } from './type-resolver';
9
- import { getCreateMutationHookName, getDeleteMutationHookName, getListQueryHookName, getSingleQueryHookName, getUpdateMutationHookName, hasValidPrimaryKey, } from './utils';
9
+ import { getCreateMutationHookName, getDeleteMutationHookName, getListQueryHookName, getSingleQueryHookName, getSubscriptionHookName, getUpdateMutationHookName, hasValidPrimaryKey, } from './utils';
10
10
  /**
11
11
  * Helper to create export * from './module' statement
12
12
  */
@@ -69,7 +69,7 @@ export function generateMutationsBarrel(tables) {
69
69
  }
70
70
  export function generateMainBarrel(tables, options = {}) {
71
71
  const opts = options;
72
- const { hasMutations = true, hasQueryKeys = false, hasMutationKeys = false, hasInvalidation = false, } = opts;
72
+ const { hasMutations = true, hasSubscriptions = false, hasQueryKeys = false, hasMutationKeys = false, hasInvalidation = false, } = opts;
73
73
  const tableNames = tables.map((tbl) => tbl.name).join(', ');
74
74
  const statements = [];
75
75
  // Client configuration (ORM wrapper with configure/getClient)
@@ -92,6 +92,10 @@ export function generateMainBarrel(tables, options = {}) {
92
92
  if (hasMutations) {
93
93
  statements.push(exportAllFrom('./mutations'));
94
94
  }
95
+ // Subscription hooks
96
+ if (hasSubscriptions) {
97
+ statements.push(exportAllFrom('./subscriptions'));
98
+ }
95
99
  // Add file header as leading comment on first statement
96
100
  if (statements.length > 0) {
97
101
  addJSDocComment(statements[0], [
@@ -201,6 +205,26 @@ export function generateMultiTargetBarrel(targetNames) {
201
205
  }
202
206
  return generateCode(statements);
203
207
  }
208
+ /**
209
+ * Generate the subscriptions/index.ts barrel file
210
+ */
211
+ export function generateSubscriptionsBarrel(tables) {
212
+ const statements = [];
213
+ for (const table of tables) {
214
+ const hookName = getSubscriptionHookName(table);
215
+ statements.push(exportAllFrom(`./${hookName}`));
216
+ }
217
+ // Connection state hook
218
+ statements.push(exportAllFrom('./useConnectionState'));
219
+ if (statements.length > 0) {
220
+ addJSDocComment(statements[0], [
221
+ 'Subscription hooks barrel export',
222
+ '@generated by @constructive-io/graphql-codegen',
223
+ 'DO NOT EDIT - changes will be overwritten',
224
+ ]);
225
+ }
226
+ return generateCode(statements);
227
+ }
204
228
  // ============================================================================
205
229
  // Custom operation barrels (includes both table and custom hooks)
206
230
  // ============================================================================
@@ -36,6 +36,7 @@ export interface GenerateResult {
36
36
  tables: number;
37
37
  queryHooks: number;
38
38
  mutationHooks: number;
39
+ subscriptionHooks: number;
39
40
  customQueryHooks: number;
40
41
  customMutationHooks: number;
41
42
  totalFiles: number;
@@ -71,7 +72,7 @@ export declare function generateAllFiles(tables: Table[], config: GraphQLSDKConf
71
72
  * (they're expected to exist in the shared types directory).
72
73
  */
73
74
  export declare function generate(options: GenerateOptions): GenerateResult;
74
- export { generateCustomMutationsBarrel, generateCustomQueriesBarrel, generateMainBarrel, generateMutationsBarrel, generateQueriesBarrel, } from './barrel';
75
+ export { generateCustomMutationsBarrel, generateCustomQueriesBarrel, generateMainBarrel, generateMutationsBarrel, generateQueriesBarrel, generateSubscriptionsBarrel, } from './barrel';
75
76
  export { generateClientFile } from './client';
76
77
  export { generateAllCustomMutationHooks, generateCustomMutationHook, } from './custom-mutations';
77
78
  export { generateAllCustomQueryHooks, generateCustomQueryHook, } from './custom-queries';
@@ -80,3 +81,4 @@ export { generateMutationKeysFile } from './mutation-keys';
80
81
  export { generateAllMutationHooks, generateCreateMutationHook, generateDeleteMutationHook, generateUpdateMutationHook, } from './mutations';
81
82
  export { generateAllQueryHooks, generateListQueryHook, generateSingleQueryHook, } from './queries';
82
83
  export { generateQueryKeysFile } from './query-keys';
84
+ export { generateAllSubscriptionHooks, generateConnectionStateHook, generateSubscriptionHook, } from './subscriptions';
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_QUERY_KEY_CONFIG } from '../../types/config';
2
- import { generateCustomMutationsBarrel, generateCustomQueriesBarrel, generateMainBarrel, generateMutationsBarrel, generateQueriesBarrel, } from './barrel';
2
+ import { generateCustomMutationsBarrel, generateCustomQueriesBarrel, generateMainBarrel, generateMutationsBarrel, generateQueriesBarrel, generateSubscriptionsBarrel, } from './barrel';
3
3
  import { generateClientFile } from './client';
4
4
  import { generateAllCustomMutationHooks } from './custom-mutations';
5
5
  import { generateAllCustomQueryHooks } from './custom-queries';
@@ -8,6 +8,7 @@ import { generateMutationKeysFile } from './mutation-keys';
8
8
  import { generateAllMutationHooks } from './mutations';
9
9
  import { generateAllQueryHooks } from './queries';
10
10
  import { generateQueryKeysFile } from './query-keys';
11
+ import { generateAllSubscriptionHooks, generateConnectionStateHook, } from './subscriptions';
11
12
  import { generateSelectionFile } from './selection';
12
13
  import { getTableNames } from './utils';
13
14
  // ============================================================================
@@ -168,12 +169,37 @@ export function generate(options) {
168
169
  : generateMutationsBarrel(tables),
169
170
  });
170
171
  }
172
+ // 8b. Generate subscription hooks (subscriptions/*.ts)
173
+ // Only generate for tables with the @realtime smart tag
174
+ const realtimeTables = tables.filter((t) => t.smartTags?.['@realtime'] !== undefined);
175
+ const subscriptionHooks = generateAllSubscriptionHooks(realtimeTables);
176
+ for (const hook of subscriptionHooks) {
177
+ files.push({
178
+ path: `subscriptions/${hook.fileName}`,
179
+ content: hook.content,
180
+ });
181
+ }
182
+ // 8c. Generate connection state hook + barrel only if any table has @realtime
183
+ const hasSubscriptions = subscriptionHooks.length > 0;
184
+ if (hasSubscriptions) {
185
+ const connectionStateHook = generateConnectionStateHook();
186
+ files.push({
187
+ path: `subscriptions/${connectionStateHook.fileName}`,
188
+ content: connectionStateHook.content,
189
+ });
190
+ // 8d. Generate subscriptions/index.ts barrel
191
+ files.push({
192
+ path: 'subscriptions/index.ts',
193
+ content: generateSubscriptionsBarrel(realtimeTables),
194
+ });
195
+ }
171
196
  // 9. Generate main index.ts barrel
172
197
  // No longer includes types.ts or schema-types.ts - hooks import from ORM directly
173
198
  files.push({
174
199
  path: 'index.ts',
175
200
  content: generateMainBarrel(tables, {
176
201
  hasMutations,
202
+ hasSubscriptions,
177
203
  hasQueryKeys,
178
204
  hasMutationKeys,
179
205
  hasInvalidation,
@@ -185,6 +211,7 @@ export function generate(options) {
185
211
  tables: tables.length,
186
212
  queryHooks: queryHooks.length,
187
213
  mutationHooks: mutationHooks.length,
214
+ subscriptionHooks: subscriptionHooks.length,
188
215
  customQueryHooks: customQueryHooks.length,
189
216
  customMutationHooks: customMutationHooks.length,
190
217
  totalFiles: files.length,
@@ -194,7 +221,7 @@ export function generate(options) {
194
221
  // ============================================================================
195
222
  // Re-exports for convenience
196
223
  // ============================================================================
197
- export { generateCustomMutationsBarrel, generateCustomQueriesBarrel, generateMainBarrel, generateMutationsBarrel, generateQueriesBarrel, } from './barrel';
224
+ export { generateCustomMutationsBarrel, generateCustomQueriesBarrel, generateMainBarrel, generateMutationsBarrel, generateQueriesBarrel, generateSubscriptionsBarrel, } from './barrel';
198
225
  export { generateClientFile } from './client';
199
226
  export { generateAllCustomMutationHooks, generateCustomMutationHook, } from './custom-mutations';
200
227
  export { generateAllCustomQueryHooks, generateCustomQueryHook, } from './custom-queries';
@@ -203,3 +230,4 @@ export { generateMutationKeysFile } from './mutation-keys';
203
230
  export { generateAllMutationHooks, generateCreateMutationHook, generateDeleteMutationHook, generateUpdateMutationHook, } from './mutations';
204
231
  export { generateAllQueryHooks, generateListQueryHook, generateSingleQueryHook, } from './queries';
205
232
  export { generateQueryKeysFile } from './query-keys';
233
+ export { generateAllSubscriptionHooks, generateConnectionStateHook, generateSubscriptionHook, } from './subscriptions';
@@ -10,6 +10,12 @@ export interface GeneratedClientFile {
10
10
  * Reads from the templates directory for proper type checking.
11
11
  */
12
12
  export declare function generateOrmClientFile(): GeneratedClientFile;
13
+ /**
14
+ * Generate the realtime.ts file (RealtimeManager + subscription types)
15
+ *
16
+ * Reads from the templates directory for proper type checking.
17
+ */
18
+ export declare function generateRealtimeFile(): GeneratedClientFile;
13
19
  /**
14
20
  * Generate the query-builder.ts file (runtime query builder)
15
21
  *
@@ -44,6 +44,17 @@ export function generateOrmClientFile() {
44
44
  content: readTemplateFile('orm-client.ts', 'ORM Client - Runtime GraphQL executor'),
45
45
  };
46
46
  }
47
+ /**
48
+ * Generate the realtime.ts file (RealtimeManager + subscription types)
49
+ *
50
+ * Reads from the templates directory for proper type checking.
51
+ */
52
+ export function generateRealtimeFile() {
53
+ return {
54
+ fileName: 'realtime.ts',
55
+ content: readTemplateFile('orm-realtime.ts', 'Realtime Manager - WebSocket subscription support'),
56
+ };
57
+ }
47
58
  /**
48
59
  * Generate the query-builder.ts file (runtime query builder)
49
60
  *