@constructive-io/graphql-codegen 4.38.2 → 4.39.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.
@@ -15,12 +15,35 @@ 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
+ } from './realtime';
45
+ export { RealtimeManager } from './realtime';
46
+
24
47
  /**
25
48
  * Default adapter that uses fetch for HTTP requests.
26
49
  *
@@ -117,6 +140,12 @@ export interface OrmClientConfig {
117
140
  fetch?: typeof globalThis.fetch;
118
141
  /** Custom adapter for GraphQL execution (overrides endpoint/headers/fetch) */
119
142
  adapter?: GraphQLAdapter;
143
+ /**
144
+ * Optional realtime (WebSocket) configuration.
145
+ * When provided, enables subscription methods on models.
146
+ * The WebSocket connection is created lazily on first subscribe().
147
+ */
148
+ realtime?: RealtimeConfig;
120
149
  }
121
150
 
122
151
  /**
@@ -135,6 +164,7 @@ export class GraphQLRequestError extends Error {
135
164
 
136
165
  export class OrmClient {
137
166
  private adapter: GraphQLAdapter;
167
+ private realtimeManager?: RealtimeManager;
138
168
 
139
169
  constructor(config: OrmClientConfig) {
140
170
  if (config.adapter) {
@@ -150,6 +180,10 @@ export class OrmClient {
150
180
  'OrmClientConfig requires either an endpoint or a custom adapter',
151
181
  );
152
182
  }
183
+
184
+ if (config.realtime) {
185
+ this.realtimeManager = new RealtimeManager(config.realtime);
186
+ }
153
187
  }
154
188
 
155
189
  async execute<T>(
@@ -159,6 +193,34 @@ export class OrmClient {
159
193
  return this.adapter.execute<T>(document, variables);
160
194
  }
161
195
 
196
+ /**
197
+ * Subscribe to a GraphQL subscription operation.
198
+ * Used by generated model subscribe() methods.
199
+ * @throws Error if realtime is not configured
200
+ */
201
+ subscribe<T>(
202
+ meta: SubscriptionFieldMeta,
203
+ document: string,
204
+ variables: Record<string, unknown>,
205
+ options: {
206
+ onEvent: (event: SubscriptionEvent<T>) => void;
207
+ onError?: (error: Error) => void;
208
+ onComplete?: () => void;
209
+ },
210
+ ): Unsubscribe {
211
+ if (!this.realtimeManager) {
212
+ throw new Error(
213
+ 'Realtime not configured. Pass a `realtime` option to createClient() to enable subscriptions.',
214
+ );
215
+ }
216
+ return this.realtimeManager.subscribe<T>(
217
+ meta,
218
+ document,
219
+ variables,
220
+ options,
221
+ );
222
+ }
223
+
162
224
  /**
163
225
  * Set headers for requests.
164
226
  * Only works if the adapter supports headers.
@@ -176,4 +238,32 @@ export class OrmClient {
176
238
  getEndpoint(): string {
177
239
  return this.adapter.getEndpoint?.() ?? '';
178
240
  }
241
+
242
+ /** Get current WebSocket connection state */
243
+ getConnectionState(): ConnectionState {
244
+ return this.realtimeManager?.getConnectionState() ?? 'disconnected';
245
+ }
246
+
247
+ /** Register a listener for WebSocket connection state changes */
248
+ onConnectionStateChange(
249
+ listener: ConnectionStateListener,
250
+ ): Unsubscribe {
251
+ if (!this.realtimeManager) return () => {};
252
+ return this.realtimeManager.onConnectionStateChange(listener);
253
+ }
254
+
255
+ /** Number of active subscriptions */
256
+ getActiveSubscriptionCount(): number {
257
+ return this.realtimeManager?.getActiveSubscriptionCount() ?? 0;
258
+ }
259
+
260
+ /** Whether realtime is configured */
261
+ get isRealtimeEnabled(): boolean {
262
+ return this.realtimeManager !== undefined;
263
+ }
264
+
265
+ /** Dispose the realtime manager (close WebSocket) */
266
+ dispose(): void {
267
+ this.realtimeManager?.dispose();
268
+ }
179
269
  }
@@ -0,0 +1,295 @@
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
+ // graphql-ws is loaded lazily so that importing this module does not
13
+ // throw when the package is absent (e.g. CLI-only consumers).
14
+ type WsClient = import('graphql-ws').Client;
15
+
16
+ // ============================================================================
17
+ // Types
18
+ // ============================================================================
19
+
20
+ /** The DML operation that triggered the subscription event */
21
+ export type SubscriptionOperation = 'INSERT' | 'UPDATE' | 'DELETE';
22
+
23
+ /** Connection state of the WebSocket */
24
+ export type ConnectionState =
25
+ | 'disconnected'
26
+ | 'connecting'
27
+ | 'connected'
28
+ | 'reconnecting';
29
+
30
+ /** Listener for connection state changes */
31
+ export type ConnectionStateListener = (state: ConnectionState) => void;
32
+
33
+ /** Function returned by subscribe() to cancel the subscription */
34
+ export type Unsubscribe = () => void;
35
+
36
+ /**
37
+ * A realtime subscription event delivered to the client.
38
+ *
39
+ * @typeParam T - The row type of the subscribed table
40
+ */
41
+ export interface SubscriptionEvent<T> {
42
+ /** The DML operation that triggered this event */
43
+ operation: SubscriptionOperation;
44
+ /** The current row data (null for DELETE if row is no longer visible) */
45
+ data: T | null;
46
+ /** Previous field values (populated on UPDATE when available) */
47
+ previousValues?: Partial<T>;
48
+ /** Server-side timestamp of when the change occurred */
49
+ timestamp: string;
50
+ }
51
+
52
+ /**
53
+ * Options for creating a subscription.
54
+ *
55
+ * @typeParam T - The row type of the subscribed table
56
+ * @typeParam TFilter - The filter type for the table
57
+ */
58
+ export interface SubscribeOptions<
59
+ T,
60
+ TFilter = Record<string, unknown>,
61
+ > {
62
+ /** Server-side filter to limit which events are delivered */
63
+ filter?: TFilter;
64
+ /** Called when a subscription event is received */
65
+ onEvent: (event: SubscriptionEvent<T>) => void;
66
+ /** Called when the subscription encounters an error */
67
+ onError?: (error: Error) => void;
68
+ /** Called when the subscription completes (server-initiated close) */
69
+ onComplete?: () => void;
70
+ }
71
+
72
+ /**
73
+ * Metadata about a subscription field, used internally to map
74
+ * table names to GraphQL subscription field names and types.
75
+ */
76
+ export interface SubscriptionFieldMeta {
77
+ /** The GraphQL subscription field name (e.g., 'onContactChanged') */
78
+ fieldName: string;
79
+ /** The table name in the source schema (e.g., 'contact') */
80
+ tableName: string;
81
+ /** The data field name inside the subscription payload (e.g., 'contact') */
82
+ dataFieldName: string;
83
+ }
84
+
85
+ /**
86
+ * Configuration for the realtime (WebSocket) connection.
87
+ * Pass this as the `realtime` option in OrmClientConfig.
88
+ */
89
+ export interface RealtimeConfig {
90
+ /** WebSocket endpoint URL (e.g., 'wss://api.example.com/graphql') */
91
+ url: string;
92
+ /**
93
+ * Returns the current auth token. Called on connection init and
94
+ * on reconnection so the client always sends a fresh token.
95
+ */
96
+ getToken?: () => string | Promise<string>;
97
+ /**
98
+ * Additional connection parameters sent during WebSocket handshake.
99
+ * Merged with the authorization header from getToken().
100
+ */
101
+ connectionParams?: Record<string, unknown>;
102
+ /**
103
+ * Whether to connect lazily (on first subscribe) or eagerly.
104
+ * @default true
105
+ */
106
+ lazy?: boolean;
107
+ /**
108
+ * Maximum number of reconnection attempts before giving up.
109
+ * @default 5
110
+ */
111
+ retryAttempts?: number;
112
+ /**
113
+ * Delay between reconnection attempts in milliseconds,
114
+ * or a function for custom backoff.
115
+ * @default 1000
116
+ */
117
+ retryWait?: number | ((retryCount: number) => number | Promise<number>);
118
+ /** Called when the WebSocket connection is established */
119
+ onConnected?: () => void;
120
+ /** Called when the WebSocket connection is closed */
121
+ onDisconnected?: (reason?: unknown) => void;
122
+ }
123
+
124
+ // ============================================================================
125
+ // RealtimeManager
126
+ // ============================================================================
127
+
128
+ /**
129
+ * Manages a single graphql-ws WebSocket connection and multiplexes
130
+ * subscriptions over it. Created lazily by OrmClient when `realtime`
131
+ * config is provided.
132
+ */
133
+ export class RealtimeManager {
134
+ private wsClient: WsClient;
135
+ private connectionState: ConnectionState = 'disconnected';
136
+ private stateListeners: Set<ConnectionStateListener> = new Set();
137
+ private activeSubscriptions = 0;
138
+
139
+ constructor(config: RealtimeConfig) {
140
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
141
+ const { createClient: createWsClient } = require('graphql-ws') as typeof import('graphql-ws');
142
+
143
+ const retryWait = async (retryCount: number): Promise<void> => {
144
+ if (typeof config.retryWait === 'function') {
145
+ const result = config.retryWait(retryCount);
146
+ const ms = typeof result === 'number' ? result : await result;
147
+ await new Promise<void>((resolve) => setTimeout(resolve, ms));
148
+ } else {
149
+ const base =
150
+ typeof config.retryWait === 'number' ? config.retryWait : 1000;
151
+ await new Promise<void>((resolve) =>
152
+ setTimeout(resolve, base * Math.pow(2, retryCount)),
153
+ );
154
+ }
155
+ };
156
+
157
+ this.wsClient = createWsClient({
158
+ url: config.url,
159
+ lazy: config.lazy ?? true,
160
+ retryAttempts: config.retryAttempts ?? 5,
161
+ retryWait,
162
+ connectionParams: async () => {
163
+ const params: Record<string, unknown> = {
164
+ ...config.connectionParams,
165
+ };
166
+ if (config.getToken) {
167
+ const token = await config.getToken();
168
+ params['authorization'] = `Bearer ${token}`;
169
+ }
170
+ return params;
171
+ },
172
+ on: {
173
+ connecting: () => {
174
+ const newState =
175
+ this.connectionState === 'disconnected'
176
+ ? 'connecting'
177
+ : 'reconnecting';
178
+ this.setConnectionState(newState);
179
+ },
180
+ connected: () => {
181
+ this.setConnectionState('connected');
182
+ config.onConnected?.();
183
+ },
184
+ closed: (event) => {
185
+ this.setConnectionState('disconnected');
186
+ config.onDisconnected?.(event);
187
+ },
188
+ },
189
+ });
190
+ }
191
+
192
+ /**
193
+ * Subscribe to a GraphQL subscription operation.
194
+ * Models call this with typed metadata and documents.
195
+ */
196
+ subscribe<T>(
197
+ meta: SubscriptionFieldMeta,
198
+ document: string,
199
+ variables: Record<string, unknown>,
200
+ options: {
201
+ onEvent: (event: SubscriptionEvent<T>) => void;
202
+ onError?: (error: Error) => void;
203
+ onComplete?: () => void;
204
+ },
205
+ ): Unsubscribe {
206
+ this.activeSubscriptions++;
207
+ let disposed = false;
208
+
209
+ const cleanup = this.wsClient.subscribe<Record<string, unknown>>(
210
+ { query: document, variables },
211
+ {
212
+ next: (result) => {
213
+ if (disposed) return;
214
+ if (result.errors) {
215
+ options.onError?.(
216
+ new Error(
217
+ result.errors.map((e) => e.message).join('; '),
218
+ ),
219
+ );
220
+ return;
221
+ }
222
+
223
+ const payload = result.data?.[meta.fieldName] as
224
+ | { event?: string; [key: string]: unknown }
225
+ | undefined;
226
+
227
+ if (!payload) return;
228
+
229
+ const event: SubscriptionEvent<T> = {
230
+ operation:
231
+ (payload.event as SubscriptionOperation) ?? 'UPDATE',
232
+ data: (payload[meta.dataFieldName] as T) ?? null,
233
+ previousValues: payload.previousValues as
234
+ | Partial<T>
235
+ | undefined,
236
+ timestamp:
237
+ (payload.timestamp as string) ?? new Date().toISOString(),
238
+ };
239
+ options.onEvent(event);
240
+ },
241
+ error: (err) => {
242
+ if (disposed) return;
243
+ options.onError?.(
244
+ err instanceof Error ? err : new Error(String(err)),
245
+ );
246
+ },
247
+ complete: () => {
248
+ if (disposed) return;
249
+ options.onComplete?.();
250
+ },
251
+ },
252
+ );
253
+
254
+ return () => {
255
+ if (disposed) return;
256
+ disposed = true;
257
+ this.activeSubscriptions--;
258
+ cleanup();
259
+ };
260
+ }
261
+
262
+ /** Register a listener for connection state changes */
263
+ onConnectionStateChange(listener: ConnectionStateListener): Unsubscribe {
264
+ this.stateListeners.add(listener);
265
+ return () => {
266
+ this.stateListeners.delete(listener);
267
+ };
268
+ }
269
+
270
+ /** Get current connection state */
271
+ getConnectionState(): ConnectionState {
272
+ return this.connectionState;
273
+ }
274
+
275
+ /** Number of active subscriptions */
276
+ getActiveSubscriptionCount(): number {
277
+ return this.activeSubscriptions;
278
+ }
279
+
280
+ /** Dispose the manager and close the WebSocket connection */
281
+ dispose(): void {
282
+ this.wsClient.dispose();
283
+ this.stateListeners.clear();
284
+ this.activeSubscriptions = 0;
285
+ this.setConnectionState('disconnected');
286
+ }
287
+
288
+ private setConnectionState(state: ConnectionState): void {
289
+ if (this.connectionState === state) return;
290
+ this.connectionState = state;
291
+ for (const listener of this.stateListeners) {
292
+ listener(state);
293
+ }
294
+ }
295
+ }
@@ -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
  *