@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.
- 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 +91 -0
- package/core/codegen/templates/orm-realtime.ts +267 -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
|
@@ -45,6 +45,37 @@ export declare class FetchAdapter implements GraphQLAdapter {
|
|
|
45
45
|
setHeaders(headers: Record<string, string>): void;
|
|
46
46
|
getEndpoint(): string;
|
|
47
47
|
}
|
|
48
|
+
export type SubscriptionOperation = 'INSERT' | 'UPDATE' | 'DELETE';
|
|
49
|
+
export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting';
|
|
50
|
+
export type ConnectionStateListener = (state: ConnectionState) => void;
|
|
51
|
+
export type Unsubscribe = () => void;
|
|
52
|
+
export interface SubscriptionEvent<T> {
|
|
53
|
+
operation: SubscriptionOperation;
|
|
54
|
+
data: T | null;
|
|
55
|
+
previousValues?: Partial<T>;
|
|
56
|
+
timestamp: string;
|
|
57
|
+
}
|
|
58
|
+
export interface SubscriptionFieldMeta {
|
|
59
|
+
fieldName: string;
|
|
60
|
+
tableName: string;
|
|
61
|
+
dataFieldName: string;
|
|
62
|
+
}
|
|
63
|
+
export interface SubscribeOptions<T, TFilter = Record<string, unknown>> {
|
|
64
|
+
filter?: TFilter;
|
|
65
|
+
onEvent: (event: SubscriptionEvent<T>) => void;
|
|
66
|
+
onError?: (error: Error) => void;
|
|
67
|
+
onComplete?: () => void;
|
|
68
|
+
}
|
|
69
|
+
export interface RealtimeConfig {
|
|
70
|
+
url: string;
|
|
71
|
+
getToken?: () => string | Promise<string>;
|
|
72
|
+
connectionParams?: Record<string, unknown>;
|
|
73
|
+
lazy?: boolean;
|
|
74
|
+
retryAttempts?: number;
|
|
75
|
+
retryWait?: number | ((retryCount: number) => number | Promise<number>);
|
|
76
|
+
onConnected?: () => void;
|
|
77
|
+
onDisconnected?: (reason?: unknown) => void;
|
|
78
|
+
}
|
|
48
79
|
/**
|
|
49
80
|
* Configuration for creating an ORM client.
|
|
50
81
|
*/
|
|
@@ -55,6 +86,8 @@ export interface OrmClientConfig {
|
|
|
55
86
|
headers?: Record<string, string>;
|
|
56
87
|
/** Custom adapter for GraphQL execution (overrides endpoint/headers) */
|
|
57
88
|
adapter?: GraphQLAdapter;
|
|
89
|
+
/** Optional realtime (WebSocket) configuration */
|
|
90
|
+
realtime?: RealtimeConfig;
|
|
58
91
|
}
|
|
59
92
|
export declare class GraphQLRequestError extends Error {
|
|
60
93
|
readonly errors: GraphQLError[];
|
|
@@ -65,6 +98,16 @@ export declare class OrmClient {
|
|
|
65
98
|
private adapter;
|
|
66
99
|
constructor(config: OrmClientConfig);
|
|
67
100
|
execute<T>(document: string, variables?: Record<string, unknown>): Promise<QueryResult<T>>;
|
|
101
|
+
subscribe<T>(meta: SubscriptionFieldMeta, document: string, variables: Record<string, unknown>, options: {
|
|
102
|
+
onEvent: (event: SubscriptionEvent<T>) => void;
|
|
103
|
+
onError?: (error: Error) => void;
|
|
104
|
+
onComplete?: () => void;
|
|
105
|
+
}): Unsubscribe;
|
|
68
106
|
setHeaders(headers: Record<string, string>): void;
|
|
69
107
|
getEndpoint(): string;
|
|
108
|
+
getConnectionState(): ConnectionState;
|
|
109
|
+
onConnectionStateChange(listener: ConnectionStateListener): Unsubscribe;
|
|
110
|
+
getActiveSubscriptionCount(): number;
|
|
111
|
+
get isRealtimeEnabled(): boolean;
|
|
112
|
+
dispose(): void;
|
|
70
113
|
}
|
|
@@ -87,6 +87,9 @@ export class OrmClient {
|
|
|
87
87
|
async execute(document, variables) {
|
|
88
88
|
return this.adapter.execute(document, variables);
|
|
89
89
|
}
|
|
90
|
+
subscribe(meta, document, variables, options) {
|
|
91
|
+
throw new Error('Realtime not configured');
|
|
92
|
+
}
|
|
90
93
|
setHeaders(headers) {
|
|
91
94
|
if (this.adapter.setHeaders) {
|
|
92
95
|
this.adapter.setHeaders(headers);
|
|
@@ -95,4 +98,17 @@ export class OrmClient {
|
|
|
95
98
|
getEndpoint() {
|
|
96
99
|
return this.adapter.getEndpoint?.() ?? '';
|
|
97
100
|
}
|
|
101
|
+
getConnectionState() {
|
|
102
|
+
return 'disconnected';
|
|
103
|
+
}
|
|
104
|
+
onConnectionStateChange(listener) {
|
|
105
|
+
return () => { };
|
|
106
|
+
}
|
|
107
|
+
getActiveSubscriptionCount() {
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
get isRealtimeEnabled() {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
dispose() { }
|
|
98
114
|
}
|
|
@@ -40,6 +40,6 @@ export interface GenerateOrmResult {
|
|
|
40
40
|
*/
|
|
41
41
|
export declare function generateOrm(options: GenerateOrmOptions): GenerateOrmResult;
|
|
42
42
|
export { generateModelsBarrel, generateTypesBarrel } from './barrel';
|
|
43
|
-
export { generateOrmClientFile, generateQueryBuilderFile, generateSelectTypesFile, } from './client-generator';
|
|
43
|
+
export { generateOrmClientFile, generateQueryBuilderFile, generateRealtimeFile, generateSelectTypesFile, } from './client-generator';
|
|
44
44
|
export { generateCustomMutationOpsFile, generateCustomQueryOpsFile, } from './custom-ops-generator';
|
|
45
45
|
export { generateAllModelFiles, generateModelFile } from './model-generator';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { generateModelsBarrel, generateTypesBarrel } from './barrel';
|
|
2
|
-
import { generateCreateClientFile, generateOrmClientFile, generateQueryBuilderFile, generateSelectTypesFile, } from './client-generator';
|
|
2
|
+
import { generateCreateClientFile, generateOrmClientFile, generateQueryBuilderFile, generateRealtimeFile, generateSelectTypesFile, } from './client-generator';
|
|
3
3
|
import { generateCustomMutationOpsFile, generateCustomQueryOpsFile, } from './custom-ops-generator';
|
|
4
4
|
import { collectInputTypeNames, collectPayloadTypeNames, generateInputTypesFile, } from './input-types-generator';
|
|
5
5
|
import { generateAllModelFiles } from './model-generator';
|
|
@@ -15,9 +15,11 @@ export function generateOrm(options) {
|
|
|
15
15
|
const hasCustomQueries = (customOperations?.queries.length ?? 0) > 0;
|
|
16
16
|
const hasCustomMutations = (customOperations?.mutations.length ?? 0) > 0;
|
|
17
17
|
const typeRegistry = customOperations?.typeRegistry;
|
|
18
|
-
// 1. Generate runtime files (client, query-builder, select-types)
|
|
18
|
+
// 1. Generate runtime files (client, query-builder, select-types, realtime)
|
|
19
19
|
const clientFile = generateOrmClientFile();
|
|
20
20
|
files.push({ path: clientFile.fileName, content: clientFile.content });
|
|
21
|
+
const realtimeFile = generateRealtimeFile();
|
|
22
|
+
files.push({ path: realtimeFile.fileName, content: realtimeFile.content });
|
|
21
23
|
const queryBuilderFile = generateQueryBuilderFile();
|
|
22
24
|
files.push({
|
|
23
25
|
path: queryBuilderFile.fileName,
|
|
@@ -102,6 +104,6 @@ export function generateOrm(options) {
|
|
|
102
104
|
}
|
|
103
105
|
// Re-export generators for direct use
|
|
104
106
|
export { generateModelsBarrel, generateTypesBarrel } from './barrel';
|
|
105
|
-
export { generateOrmClientFile, generateQueryBuilderFile, generateSelectTypesFile, } from './client-generator';
|
|
107
|
+
export { generateOrmClientFile, generateQueryBuilderFile, generateRealtimeFile, generateSelectTypesFile, } from './client-generator';
|
|
106
108
|
export { generateCustomMutationOpsFile, generateCustomQueryOpsFile, } from './custom-ops-generator';
|
|
107
109
|
export { generateAllModelFiles, generateModelFile } from './model-generator';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Table } from '../../types/schema';
|
|
2
|
+
export interface GeneratedSubscriptionFile {
|
|
3
|
+
fileName: string;
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Generate a subscription hook for a table.
|
|
8
|
+
*
|
|
9
|
+
* Produces a React hook that calls `getClient().subscribe()` with the
|
|
10
|
+
* correct subscription document, field metadata, and typed callbacks.
|
|
11
|
+
*
|
|
12
|
+
* Example generated output:
|
|
13
|
+
* ```ts
|
|
14
|
+
* export function useContactSubscription(options: ContactSubscriptionOptions): Unsubscribe {
|
|
15
|
+
* ...
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare function generateSubscriptionHook(table: Table): GeneratedSubscriptionFile;
|
|
20
|
+
/**
|
|
21
|
+
* Generate the useConnectionState hook file.
|
|
22
|
+
*
|
|
23
|
+
* This hook exposes the WebSocket connection state from the ORM client
|
|
24
|
+
* so UI components can show connection indicators.
|
|
25
|
+
*/
|
|
26
|
+
export declare function generateConnectionStateHook(): GeneratedSubscriptionFile;
|
|
27
|
+
/**
|
|
28
|
+
* Generate subscription hooks for all tables
|
|
29
|
+
*/
|
|
30
|
+
export declare function generateAllSubscriptionHooks(tables: Table[]): GeneratedSubscriptionFile[];
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription hook generators - delegates to ORM client subscribe (Babel AST-based)
|
|
3
|
+
*
|
|
4
|
+
* Output structure:
|
|
5
|
+
* subscriptions/
|
|
6
|
+
* useContactSubscription.ts - Subscription hook -> ORM client.subscribe()
|
|
7
|
+
* useConnectionState.ts - Connection state hook
|
|
8
|
+
*/
|
|
9
|
+
import * as t from '@babel/types';
|
|
10
|
+
import { addJSDocComment, callExpr, constDecl, createFunctionParam, createImportDeclaration, createTypeReExport, exportFunction, generateHookFileCode, objectProp, typeRef, } from './hooks-ast';
|
|
11
|
+
import { getSubscriptionFieldName, getSubscriptionFileName, getSubscriptionHookName, getTableNames, lcFirst, } from './utils';
|
|
12
|
+
/**
|
|
13
|
+
* Generate a subscription hook for a table.
|
|
14
|
+
*
|
|
15
|
+
* Produces a React hook that calls `getClient().subscribe()` with the
|
|
16
|
+
* correct subscription document, field metadata, and typed callbacks.
|
|
17
|
+
*
|
|
18
|
+
* Example generated output:
|
|
19
|
+
* ```ts
|
|
20
|
+
* export function useContactSubscription(options: ContactSubscriptionOptions): Unsubscribe {
|
|
21
|
+
* ...
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function generateSubscriptionHook(table) {
|
|
26
|
+
const { typeName, singularName } = getTableNames(table);
|
|
27
|
+
const hookName = getSubscriptionHookName(table);
|
|
28
|
+
const subscriptionFieldName = getSubscriptionFieldName(table);
|
|
29
|
+
const keysName = `${lcFirst(typeName)}Keys`;
|
|
30
|
+
const statements = [];
|
|
31
|
+
// Imports
|
|
32
|
+
statements.push(createImportDeclaration('react', ['useEffect', 'useRef', 'useCallback']));
|
|
33
|
+
statements.push(createImportDeclaration('@tanstack/react-query', ['useQueryClient']));
|
|
34
|
+
statements.push(createImportDeclaration('@tanstack/react-query', ['QueryClient'], true));
|
|
35
|
+
statements.push(createImportDeclaration('../client', ['getClient']));
|
|
36
|
+
statements.push(createImportDeclaration('../../orm/client', [
|
|
37
|
+
'SubscriptionEvent',
|
|
38
|
+
'SubscriptionFieldMeta',
|
|
39
|
+
'Unsubscribe',
|
|
40
|
+
], true));
|
|
41
|
+
statements.push(createImportDeclaration('../../orm/input-types', [typeName], true));
|
|
42
|
+
statements.push(createImportDeclaration('../query-keys', [keysName]));
|
|
43
|
+
// Re-export SubscriptionEvent for consumer convenience
|
|
44
|
+
statements.push(createTypeReExport(['SubscriptionEvent', 'Unsubscribe'], '../../orm/client'));
|
|
45
|
+
// Subscription document constant
|
|
46
|
+
const subscriptionDoc = `subscription On${typeName}Changed {
|
|
47
|
+
${subscriptionFieldName} {
|
|
48
|
+
event
|
|
49
|
+
${singularName} { __typename }
|
|
50
|
+
timestamp
|
|
51
|
+
}
|
|
52
|
+
}`;
|
|
53
|
+
const docDecl = constDecl('SUBSCRIPTION_DOCUMENT', t.stringLiteral(subscriptionDoc));
|
|
54
|
+
statements.push(docDecl);
|
|
55
|
+
// Field metadata constant
|
|
56
|
+
const metaDecl = t.variableDeclaration('const', [
|
|
57
|
+
t.variableDeclarator(t.identifier('FIELD_META'), t.objectExpression([
|
|
58
|
+
objectProp('fieldName', t.stringLiteral(subscriptionFieldName)),
|
|
59
|
+
objectProp('tableName', t.stringLiteral(singularName)),
|
|
60
|
+
objectProp('dataFieldName', t.stringLiteral(singularName)),
|
|
61
|
+
])),
|
|
62
|
+
]);
|
|
63
|
+
// Add type annotation: SubscriptionFieldMeta
|
|
64
|
+
const metaId = metaDecl.declarations[0].id;
|
|
65
|
+
metaId.typeAnnotation = t.tsTypeAnnotation(typeRef('SubscriptionFieldMeta'));
|
|
66
|
+
statements.push(metaDecl);
|
|
67
|
+
// Options interface
|
|
68
|
+
const optionsTypeName = `${typeName}SubscriptionOptions`;
|
|
69
|
+
const optionsInterface = t.tsInterfaceDeclaration(t.identifier(optionsTypeName), null, null, t.tsInterfaceBody([
|
|
70
|
+
(() => {
|
|
71
|
+
const p = t.tsPropertySignature(t.identifier('onEvent'), t.tsTypeAnnotation(t.tsFunctionType(null, [
|
|
72
|
+
createFunctionParam('event', typeRef('SubscriptionEvent', [typeRef(typeName)])),
|
|
73
|
+
], t.tsTypeAnnotation(t.tsVoidKeyword()))));
|
|
74
|
+
return p;
|
|
75
|
+
})(),
|
|
76
|
+
(() => {
|
|
77
|
+
const p = t.tsPropertySignature(t.identifier('onError'), t.tsTypeAnnotation(t.tsFunctionType(null, [createFunctionParam('error', typeRef('Error'))], t.tsTypeAnnotation(t.tsVoidKeyword()))));
|
|
78
|
+
p.optional = true;
|
|
79
|
+
return p;
|
|
80
|
+
})(),
|
|
81
|
+
(() => {
|
|
82
|
+
const p = t.tsPropertySignature(t.identifier('enabled'), t.tsTypeAnnotation(t.tsBooleanKeyword()));
|
|
83
|
+
p.optional = true;
|
|
84
|
+
return p;
|
|
85
|
+
})(),
|
|
86
|
+
(() => {
|
|
87
|
+
const p = t.tsPropertySignature(t.identifier('invalidateQueries'), t.tsTypeAnnotation(t.tsBooleanKeyword()));
|
|
88
|
+
p.optional = true;
|
|
89
|
+
return p;
|
|
90
|
+
})(),
|
|
91
|
+
]));
|
|
92
|
+
statements.push(t.exportNamedDeclaration(optionsInterface));
|
|
93
|
+
// Hook implementation
|
|
94
|
+
const hookBody = [];
|
|
95
|
+
// const queryClient = useQueryClient();
|
|
96
|
+
hookBody.push(constDecl('queryClient', callExpr('useQueryClient', [])));
|
|
97
|
+
// const optionsRef = useRef(options);
|
|
98
|
+
hookBody.push(constDecl('optionsRef', callExpr('useRef', [t.identifier('options')])));
|
|
99
|
+
// optionsRef.current = options;
|
|
100
|
+
hookBody.push(t.expressionStatement(t.assignmentExpression('=', t.memberExpression(t.identifier('optionsRef'), t.identifier('current')), t.identifier('options'))));
|
|
101
|
+
// useEffect with subscribe
|
|
102
|
+
const effectBody = [];
|
|
103
|
+
// if (options.enabled === false) return;
|
|
104
|
+
effectBody.push(t.ifStatement(t.binaryExpression('===', t.memberExpression(t.identifier('options'), t.identifier('enabled')), t.booleanLiteral(false)), t.returnStatement(null)));
|
|
105
|
+
// const client = getClient();
|
|
106
|
+
effectBody.push(constDecl('client', callExpr('getClient', [])));
|
|
107
|
+
// if (!client.isRealtimeEnabled) return;
|
|
108
|
+
effectBody.push(t.ifStatement(t.unaryExpression('!', t.memberExpression(t.identifier('client'), t.identifier('isRealtimeEnabled'))), t.returnStatement(null)));
|
|
109
|
+
// const unsubscribe = client.subscribe(FIELD_META, SUBSCRIPTION_DOCUMENT, {}, { onEvent, onError, onComplete });
|
|
110
|
+
const subscribeCall = t.callExpression(t.memberExpression(t.identifier('client'), t.identifier('subscribe')), [
|
|
111
|
+
t.identifier('FIELD_META'),
|
|
112
|
+
t.identifier('SUBSCRIPTION_DOCUMENT'),
|
|
113
|
+
t.objectExpression([]),
|
|
114
|
+
t.objectExpression([
|
|
115
|
+
objectProp('onEvent', t.arrowFunctionExpression([t.identifier('event')], t.blockStatement([
|
|
116
|
+
// optionsRef.current.onEvent(event);
|
|
117
|
+
t.expressionStatement(callExpr(t.memberExpression(t.memberExpression(t.identifier('optionsRef'), t.identifier('current')), t.identifier('onEvent')), [t.identifier('event')])),
|
|
118
|
+
// if (optionsRef.current.invalidateQueries !== false) { queryClient.invalidateQueries({ queryKey: keysName.all }); }
|
|
119
|
+
t.ifStatement(t.binaryExpression('!==', t.memberExpression(t.memberExpression(t.identifier('optionsRef'), t.identifier('current')), t.identifier('invalidateQueries')), t.booleanLiteral(false)), t.expressionStatement(callExpr(t.memberExpression(t.identifier('queryClient'), t.identifier('invalidateQueries')), [
|
|
120
|
+
t.objectExpression([
|
|
121
|
+
objectProp('queryKey', t.memberExpression(t.identifier(keysName), t.identifier('all'))),
|
|
122
|
+
]),
|
|
123
|
+
]))),
|
|
124
|
+
]))),
|
|
125
|
+
objectProp('onError', t.arrowFunctionExpression([t.identifier('err')], t.blockStatement([
|
|
126
|
+
t.expressionStatement(t.optionalCallExpression(t.optionalMemberExpression(t.memberExpression(t.identifier('optionsRef'), t.identifier('current')), t.identifier('onError'), false, true), [t.identifier('err')], false)),
|
|
127
|
+
]))),
|
|
128
|
+
]),
|
|
129
|
+
]);
|
|
130
|
+
effectBody.push(constDecl('unsubscribe', subscribeCall));
|
|
131
|
+
// return () => unsubscribe();
|
|
132
|
+
effectBody.push(t.returnStatement(t.arrowFunctionExpression([], t.callExpression(t.identifier('unsubscribe'), []))));
|
|
133
|
+
// useEffect(() => { ... }, [options.enabled]);
|
|
134
|
+
const effectFn = t.arrowFunctionExpression([], t.blockStatement(effectBody));
|
|
135
|
+
hookBody.push(t.expressionStatement(callExpr('useEffect', [
|
|
136
|
+
effectFn,
|
|
137
|
+
t.arrayExpression([
|
|
138
|
+
t.memberExpression(t.identifier('options'), t.identifier('enabled')),
|
|
139
|
+
t.identifier('queryClient'),
|
|
140
|
+
]),
|
|
141
|
+
])));
|
|
142
|
+
// Hook declaration
|
|
143
|
+
const hookParam = createFunctionParam('options', typeRef(optionsTypeName));
|
|
144
|
+
const hookDecl = exportFunction(hookName, null, [hookParam], hookBody, t.tsVoidKeyword());
|
|
145
|
+
addJSDocComment(hookDecl, [
|
|
146
|
+
`Subscription hook for ${typeName} realtime events`,
|
|
147
|
+
'',
|
|
148
|
+
'Subscribes to realtime changes on the server and automatically',
|
|
149
|
+
'invalidates React Query cache when events are received.',
|
|
150
|
+
'',
|
|
151
|
+
'@example',
|
|
152
|
+
'```tsx',
|
|
153
|
+
`${hookName}({`,
|
|
154
|
+
' onEvent: (event) => {',
|
|
155
|
+
` console.log(event.operation, event.data);`,
|
|
156
|
+
' },',
|
|
157
|
+
'});',
|
|
158
|
+
'```',
|
|
159
|
+
]);
|
|
160
|
+
statements.push(hookDecl);
|
|
161
|
+
return {
|
|
162
|
+
fileName: getSubscriptionFileName(table),
|
|
163
|
+
content: generateHookFileCode(`Subscription hook for ${typeName}`, statements),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Generate the useConnectionState hook file.
|
|
168
|
+
*
|
|
169
|
+
* This hook exposes the WebSocket connection state from the ORM client
|
|
170
|
+
* so UI components can show connection indicators.
|
|
171
|
+
*/
|
|
172
|
+
export function generateConnectionStateHook() {
|
|
173
|
+
const statements = [];
|
|
174
|
+
// Imports
|
|
175
|
+
statements.push(createImportDeclaration('react', ['useState', 'useEffect']));
|
|
176
|
+
statements.push(createImportDeclaration('../client', ['getClient']));
|
|
177
|
+
statements.push(createImportDeclaration('../../orm/client', ['ConnectionState'], true));
|
|
178
|
+
// Re-export ConnectionState
|
|
179
|
+
statements.push(createTypeReExport(['ConnectionState'], '../../orm/client'));
|
|
180
|
+
// Hook body
|
|
181
|
+
const hookBody = [];
|
|
182
|
+
// const [state, setState] = useState<ConnectionState>(() => getClient().getConnectionState());
|
|
183
|
+
const initFn = t.arrowFunctionExpression([], callExpr(t.memberExpression(callExpr('getClient', []), t.identifier('getConnectionState')), []));
|
|
184
|
+
const useStateCall = callExpr('useState', [initFn]);
|
|
185
|
+
// @ts-ignore - typeParameters on CallExpression for TS
|
|
186
|
+
useStateCall.typeParameters = t.tsTypeParameterInstantiation([
|
|
187
|
+
typeRef('ConnectionState'),
|
|
188
|
+
]);
|
|
189
|
+
hookBody.push(t.variableDeclaration('const', [
|
|
190
|
+
t.variableDeclarator(t.arrayPattern([t.identifier('state'), t.identifier('setState')]), useStateCall),
|
|
191
|
+
]));
|
|
192
|
+
// useEffect
|
|
193
|
+
const effectBody = [];
|
|
194
|
+
effectBody.push(constDecl('client', callExpr('getClient', [])));
|
|
195
|
+
// if (!client.isRealtimeEnabled) return;
|
|
196
|
+
effectBody.push(t.ifStatement(t.unaryExpression('!', t.memberExpression(t.identifier('client'), t.identifier('isRealtimeEnabled'))), t.returnStatement(null)));
|
|
197
|
+
// const unsubscribe = client.onConnectionStateChange(setState);
|
|
198
|
+
effectBody.push(constDecl('unsubscribe', callExpr(t.memberExpression(t.identifier('client'), t.identifier('onConnectionStateChange')), [t.identifier('setState')])));
|
|
199
|
+
// return () => unsubscribe();
|
|
200
|
+
effectBody.push(t.returnStatement(t.arrowFunctionExpression([], t.callExpression(t.identifier('unsubscribe'), []))));
|
|
201
|
+
hookBody.push(t.expressionStatement(callExpr('useEffect', [
|
|
202
|
+
t.arrowFunctionExpression([], t.blockStatement(effectBody)),
|
|
203
|
+
t.arrayExpression([]),
|
|
204
|
+
])));
|
|
205
|
+
// return state;
|
|
206
|
+
hookBody.push(t.returnStatement(t.identifier('state')));
|
|
207
|
+
// Hook declaration
|
|
208
|
+
const hookDecl = exportFunction('useConnectionState', null, [], hookBody, typeRef('ConnectionState'));
|
|
209
|
+
addJSDocComment(hookDecl, [
|
|
210
|
+
'Hook to observe the WebSocket connection state.',
|
|
211
|
+
'',
|
|
212
|
+
'Returns the current connection state of the realtime WebSocket.',
|
|
213
|
+
"Returns 'disconnected' if realtime is not configured.",
|
|
214
|
+
'',
|
|
215
|
+
'@example',
|
|
216
|
+
'```tsx',
|
|
217
|
+
'const state = useConnectionState();',
|
|
218
|
+
"// state: 'disconnected' | 'connecting' | 'connected' | 'reconnecting'",
|
|
219
|
+
'```',
|
|
220
|
+
]);
|
|
221
|
+
statements.push(hookDecl);
|
|
222
|
+
return {
|
|
223
|
+
fileName: 'useConnectionState.ts',
|
|
224
|
+
content: generateHookFileCode('WebSocket connection state hook', statements),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Generate subscription hooks for all tables
|
|
229
|
+
*/
|
|
230
|
+
export function generateAllSubscriptionHooks(tables) {
|
|
231
|
+
return tables.map((table) => generateSubscriptionHook(table));
|
|
232
|
+
}
|
|
@@ -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
|
|
@@ -94,6 +94,29 @@ export function getUpdateMutationFileName(table) {
|
|
|
94
94
|
export function getDeleteMutationFileName(table) {
|
|
95
95
|
return `${getDeleteMutationHookName(table)}.ts`;
|
|
96
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Generate hook function name for subscription
|
|
99
|
+
* e.g., "useContactSubscription"
|
|
100
|
+
*/
|
|
101
|
+
export function getSubscriptionHookName(table) {
|
|
102
|
+
const { singularName } = getTableNames(table);
|
|
103
|
+
return `use${ucFirst(singularName)}Subscription`;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Generate file name for subscription hook
|
|
107
|
+
* e.g., "useContactSubscription.ts"
|
|
108
|
+
*/
|
|
109
|
+
export function getSubscriptionFileName(table) {
|
|
110
|
+
return `${getSubscriptionHookName(table)}.ts`;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Generate the GraphQL subscription field name
|
|
114
|
+
* e.g., "onContactChanged"
|
|
115
|
+
*/
|
|
116
|
+
export function getSubscriptionFieldName(table) {
|
|
117
|
+
const { singularName } = getTableNames(table);
|
|
118
|
+
return `on${ucFirst(singularName)}Changed`;
|
|
119
|
+
}
|
|
97
120
|
// ============================================================================
|
|
98
121
|
// GraphQL operation names
|
|
99
122
|
// ============================================================================
|
package/esm/types/schema.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface Table {
|
|
|
17
17
|
query?: TableQueryNames;
|
|
18
18
|
/** Constraint information */
|
|
19
19
|
constraints?: TableConstraints;
|
|
20
|
+
/** Smart tags parsed from PostGraphile @-prefixed comment directives */
|
|
21
|
+
smartTags?: Record<string, string | true>;
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
22
24
|
* PostGraphile-generated names for this table
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constructive-io/graphql-codegen",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.39.1",
|
|
4
4
|
"description": "GraphQL SDK generator for Constructive databases with React Query hooks",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"graphql",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"@0no-co/graphql.web": "^1.1.2",
|
|
57
57
|
"@babel/generator": "^7.29.1",
|
|
58
58
|
"@babel/types": "^7.29.0",
|
|
59
|
-
"@constructive-io/graphql-query": "^3.
|
|
59
|
+
"@constructive-io/graphql-query": "^3.22.0",
|
|
60
60
|
"@constructive-io/graphql-types": "^3.8.0",
|
|
61
61
|
"@inquirerer/utils": "^3.3.5",
|
|
62
62
|
"@pgpmjs/core": "^6.16.1",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"deepmerge": "^4.3.1",
|
|
65
65
|
"find-and-require-package-json": "^0.9.1",
|
|
66
66
|
"gql-ast": "^3.8.0",
|
|
67
|
-
"graphile-schema": "^1.18.
|
|
67
|
+
"graphile-schema": "^1.18.6",
|
|
68
68
|
"graphql": "16.13.0",
|
|
69
69
|
"inflekt": "^0.7.1",
|
|
70
70
|
"inquirerer": "^4.7.0",
|
|
@@ -100,5 +100,5 @@
|
|
|
100
100
|
"tsx": "^4.21.0",
|
|
101
101
|
"typescript": "^5.9.3"
|
|
102
102
|
},
|
|
103
|
-
"gitHead": "
|
|
103
|
+
"gitHead": "c87dafec1ccb2c3cc62f2304584104db108a810b"
|
|
104
104
|
}
|
package/types/schema.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface Table {
|
|
|
17
17
|
query?: TableQueryNames;
|
|
18
18
|
/** Constraint information */
|
|
19
19
|
constraints?: TableConstraints;
|
|
20
|
+
/** Smart tags parsed from PostGraphile @-prefixed comment directives */
|
|
21
|
+
smartTags?: Record<string, string | true>;
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
22
24
|
* PostGraphile-generated names for this table
|