@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.
- package/core/codegen/barrel.d.ts +6 -0
- package/core/codegen/barrel.js +26 -1
- package/core/codegen/index.d.ts +3 -1
- package/core/codegen/index.js +33 -1
- package/core/codegen/orm/client-generator.d.ts +6 -0
- package/core/codegen/orm/client-generator.js +12 -0
- package/core/codegen/orm/client.d.ts +43 -0
- package/core/codegen/orm/client.js +16 -0
- package/core/codegen/orm/index.d.ts +1 -1
- package/core/codegen/orm/index.js +5 -2
- package/core/codegen/subscriptions.d.ts +30 -0
- package/core/codegen/subscriptions.js +270 -0
- package/core/codegen/templates/orm-client.ts +90 -0
- package/core/codegen/templates/orm-realtime.ts +295 -0
- package/core/codegen/utils.d.ts +15 -0
- package/core/codegen/utils.js +26 -0
- package/esm/core/codegen/barrel.d.ts +6 -0
- package/esm/core/codegen/barrel.js +26 -2
- package/esm/core/codegen/index.d.ts +3 -1
- package/esm/core/codegen/index.js +30 -2
- package/esm/core/codegen/orm/client-generator.d.ts +6 -0
- package/esm/core/codegen/orm/client-generator.js +11 -0
- package/esm/core/codegen/orm/client.d.ts +43 -0
- package/esm/core/codegen/orm/client.js +16 -0
- package/esm/core/codegen/orm/index.d.ts +1 -1
- package/esm/core/codegen/orm/index.js +5 -3
- package/esm/core/codegen/subscriptions.d.ts +30 -0
- package/esm/core/codegen/subscriptions.js +232 -0
- package/esm/core/codegen/utils.d.ts +15 -0
- package/esm/core/codegen/utils.js +23 -0
- package/esm/types/schema.d.ts +2 -0
- package/package.json +4 -4
- package/types/schema.d.ts +2 -0
|
@@ -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
|
+
}
|
package/core/codegen/utils.d.ts
CHANGED
|
@@ -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
|
package/core/codegen/utils.js
CHANGED
|
@@ -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
|
*
|