@0xobelisk/graphql-client 1.2.0-pre.24
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/LICENSE +92 -0
- package/dist/client.d.ts +221 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1582 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1560 -0
- package/dist/index.mjs.map +1 -0
- package/dist/types.d.ts +211 -0
- package/dist/utils.d.ts +2 -0
- package/package.json +154 -0
- package/src/client.ts +1209 -0
- package/src/index.ts +7 -0
- package/src/types.ts +294 -0
- package/src/utils.ts +49 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,1209 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApolloClient,
|
|
3
|
+
InMemoryCache,
|
|
4
|
+
gql,
|
|
5
|
+
createHttpLink,
|
|
6
|
+
split,
|
|
7
|
+
NormalizedCacheObject,
|
|
8
|
+
WatchQueryOptions,
|
|
9
|
+
QueryOptions as ApolloQueryOptions,
|
|
10
|
+
SubscriptionOptions as ApolloSubscriptionOptions,
|
|
11
|
+
Observable,
|
|
12
|
+
from,
|
|
13
|
+
ApolloLink,
|
|
14
|
+
FetchPolicy,
|
|
15
|
+
OperationVariables,
|
|
16
|
+
} from '@apollo/client';
|
|
17
|
+
import { RetryLink } from '@apollo/client/link/retry';
|
|
18
|
+
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
|
|
19
|
+
import { getMainDefinition } from '@apollo/client/utilities';
|
|
20
|
+
import { createClient } from 'graphql-ws';
|
|
21
|
+
import pluralize from 'pluralize';
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
DubheClientConfig,
|
|
25
|
+
Connection,
|
|
26
|
+
BaseQueryParams,
|
|
27
|
+
OrderBy,
|
|
28
|
+
JsonPathOrder,
|
|
29
|
+
QueryOptions,
|
|
30
|
+
QueryResult,
|
|
31
|
+
SubscriptionResult,
|
|
32
|
+
SubscriptionOptions,
|
|
33
|
+
StringFilter,
|
|
34
|
+
NumberFilter,
|
|
35
|
+
DateFilter,
|
|
36
|
+
StoreTableRow,
|
|
37
|
+
TypedDocumentNode,
|
|
38
|
+
CachePolicy,
|
|
39
|
+
MultiTableSubscriptionConfig,
|
|
40
|
+
MultiTableSubscriptionResult,
|
|
41
|
+
MultiTableSubscriptionData,
|
|
42
|
+
ParsedTableInfo,
|
|
43
|
+
DubheMetadata,
|
|
44
|
+
} from './types';
|
|
45
|
+
|
|
46
|
+
// Convert cache policy type
|
|
47
|
+
function mapCachePolicyToFetchPolicy(cachePolicy: CachePolicy): FetchPolicy {
|
|
48
|
+
switch (cachePolicy) {
|
|
49
|
+
case 'cache-first':
|
|
50
|
+
return 'cache-first';
|
|
51
|
+
case 'network-only':
|
|
52
|
+
return 'network-only';
|
|
53
|
+
case 'cache-only':
|
|
54
|
+
return 'cache-only';
|
|
55
|
+
case 'no-cache':
|
|
56
|
+
return 'no-cache';
|
|
57
|
+
case 'standby':
|
|
58
|
+
return 'standby';
|
|
59
|
+
default:
|
|
60
|
+
return 'cache-first';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class DubheGraphqlClient {
|
|
65
|
+
private apolloClient: ApolloClient<NormalizedCacheObject>;
|
|
66
|
+
private subscriptionClient?: any;
|
|
67
|
+
private dubheMetadata?: DubheMetadata;
|
|
68
|
+
private parsedTables: Map<string, ParsedTableInfo> = new Map();
|
|
69
|
+
|
|
70
|
+
constructor(config: DubheClientConfig) {
|
|
71
|
+
// Save dubhe metadata
|
|
72
|
+
this.dubheMetadata = config.dubheMetadata;
|
|
73
|
+
|
|
74
|
+
// If dubhe metadata is provided, parse table information
|
|
75
|
+
if (this.dubheMetadata) {
|
|
76
|
+
this.parseTableInfoFromConfig();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Create HTTP Link
|
|
80
|
+
const httpLink = createHttpLink({
|
|
81
|
+
uri: config.endpoint,
|
|
82
|
+
headers: config.headers,
|
|
83
|
+
fetch: (input, init) => fetch(input, { ...config.fetchOptions, ...init }),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Create retry link
|
|
87
|
+
const retryLink = new RetryLink({
|
|
88
|
+
delay: {
|
|
89
|
+
// Initial retry delay time (milliseconds)
|
|
90
|
+
initial: config.retryOptions?.delay?.initial || 300,
|
|
91
|
+
// Maximum retry delay time (milliseconds)
|
|
92
|
+
max: config.retryOptions?.delay?.max || 5000,
|
|
93
|
+
// Whether to add random jitter to avoid thundering herd, enabled by default
|
|
94
|
+
jitter: config.retryOptions?.delay?.jitter !== false,
|
|
95
|
+
},
|
|
96
|
+
attempts: {
|
|
97
|
+
// Maximum number of attempts (including initial request)
|
|
98
|
+
max: config.retryOptions?.attempts?.max || 5,
|
|
99
|
+
// Custom retry condition function
|
|
100
|
+
retryIf:
|
|
101
|
+
config.retryOptions?.attempts?.retryIf ||
|
|
102
|
+
((error, _operation) => {
|
|
103
|
+
// Default retry strategy:
|
|
104
|
+
// 1. Network connection errors
|
|
105
|
+
// 2. Server errors but no GraphQL errors (indicates service temporarily unavailable)
|
|
106
|
+
return Boolean(
|
|
107
|
+
error &&
|
|
108
|
+
(error.networkError ||
|
|
109
|
+
(error.graphQLErrors && error.graphQLErrors.length === 0))
|
|
110
|
+
);
|
|
111
|
+
}),
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Combine HTTP link and retry link
|
|
116
|
+
const httpWithRetryLink = from([retryLink, httpLink]);
|
|
117
|
+
|
|
118
|
+
let link: ApolloLink = httpWithRetryLink;
|
|
119
|
+
|
|
120
|
+
// If subscription endpoint is provided, create WebSocket Link
|
|
121
|
+
if (config.subscriptionEndpoint) {
|
|
122
|
+
// Automatically import ws module in Node.js environment
|
|
123
|
+
let webSocketImpl;
|
|
124
|
+
try {
|
|
125
|
+
// Check if in Node.js environment
|
|
126
|
+
if (typeof window === 'undefined' && typeof global !== 'undefined') {
|
|
127
|
+
// Node.js environment, need to import ws
|
|
128
|
+
const wsModule = require('ws');
|
|
129
|
+
webSocketImpl = wsModule.default || wsModule;
|
|
130
|
+
|
|
131
|
+
// Set global WebSocket in Node.js environment to avoid apollo client internal errors
|
|
132
|
+
if (typeof (global as any).WebSocket === 'undefined') {
|
|
133
|
+
(global as any).WebSocket = webSocketImpl;
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
// Browser environment, use native WebSocket
|
|
137
|
+
webSocketImpl = WebSocket;
|
|
138
|
+
}
|
|
139
|
+
} catch (error) {
|
|
140
|
+
// Ignore ws import errors
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const clientOptions: any = {
|
|
144
|
+
url: config.subscriptionEndpoint,
|
|
145
|
+
connectionParams: {
|
|
146
|
+
headers: config.headers,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Only add webSocketImpl if in Node.js environment and ws was successfully imported
|
|
151
|
+
if (webSocketImpl && typeof window === 'undefined') {
|
|
152
|
+
clientOptions.webSocketImpl = webSocketImpl;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
this.subscriptionClient = createClient(clientOptions);
|
|
156
|
+
|
|
157
|
+
const wsLink = new GraphQLWsLink(this.subscriptionClient);
|
|
158
|
+
|
|
159
|
+
// Use split to decide which link to use
|
|
160
|
+
link = split(
|
|
161
|
+
({ query }) => {
|
|
162
|
+
const definition = getMainDefinition(query);
|
|
163
|
+
return (
|
|
164
|
+
definition.kind === 'OperationDefinition' &&
|
|
165
|
+
definition.operation === 'subscription'
|
|
166
|
+
);
|
|
167
|
+
},
|
|
168
|
+
wsLink,
|
|
169
|
+
httpWithRetryLink
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Create Apollo Client instance
|
|
174
|
+
this.apolloClient = new ApolloClient({
|
|
175
|
+
link,
|
|
176
|
+
cache:
|
|
177
|
+
config.cacheConfig?.paginatedTables &&
|
|
178
|
+
config.cacheConfig.paginatedTables.length > 0
|
|
179
|
+
? new InMemoryCache({
|
|
180
|
+
typePolicies: {
|
|
181
|
+
// Configure cache strategy for Connection type
|
|
182
|
+
Query: {
|
|
183
|
+
fields: this.buildCacheFields(config.cacheConfig),
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
})
|
|
187
|
+
: new InMemoryCache(), // Use simple cache by default
|
|
188
|
+
defaultOptions: {
|
|
189
|
+
watchQuery: {
|
|
190
|
+
errorPolicy: 'all',
|
|
191
|
+
notifyOnNetworkStatusChange: true,
|
|
192
|
+
},
|
|
193
|
+
query: {
|
|
194
|
+
errorPolicy: 'all',
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Execute GraphQL query
|
|
202
|
+
*/
|
|
203
|
+
async query<
|
|
204
|
+
TData,
|
|
205
|
+
TVariables extends OperationVariables = OperationVariables,
|
|
206
|
+
>(
|
|
207
|
+
query: TypedDocumentNode<TData, TVariables>,
|
|
208
|
+
variables?: TVariables,
|
|
209
|
+
options?: QueryOptions
|
|
210
|
+
): Promise<QueryResult<TData>> {
|
|
211
|
+
try {
|
|
212
|
+
const result = await this.apolloClient.query({
|
|
213
|
+
query,
|
|
214
|
+
variables,
|
|
215
|
+
fetchPolicy: options?.cachePolicy
|
|
216
|
+
? mapCachePolicyToFetchPolicy(options.cachePolicy)
|
|
217
|
+
: 'no-cache',
|
|
218
|
+
// : 'cache-first',
|
|
219
|
+
notifyOnNetworkStatusChange: options?.notifyOnNetworkStatusChange,
|
|
220
|
+
pollInterval: options?.pollInterval,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
data: result.data,
|
|
225
|
+
loading: result.loading,
|
|
226
|
+
error: result.error,
|
|
227
|
+
networkStatus: result.networkStatus,
|
|
228
|
+
refetch: () => this.query(query, variables, options),
|
|
229
|
+
};
|
|
230
|
+
} catch (error) {
|
|
231
|
+
return {
|
|
232
|
+
data: undefined,
|
|
233
|
+
loading: false,
|
|
234
|
+
error: error as Error,
|
|
235
|
+
networkStatus: 8, // NetworkStatus.error
|
|
236
|
+
refetch: () => this.query(query, variables, options),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Execute GraphQL subscription
|
|
243
|
+
*/
|
|
244
|
+
subscribe<TData, TVariables extends OperationVariables = OperationVariables>(
|
|
245
|
+
subscription: TypedDocumentNode<TData, TVariables>,
|
|
246
|
+
variables?: TVariables,
|
|
247
|
+
options?: SubscriptionOptions
|
|
248
|
+
): Observable<SubscriptionResult<TData>> {
|
|
249
|
+
return new Observable((observer: any) => {
|
|
250
|
+
const sub = this.apolloClient
|
|
251
|
+
.subscribe({
|
|
252
|
+
query: subscription,
|
|
253
|
+
variables,
|
|
254
|
+
})
|
|
255
|
+
.subscribe({
|
|
256
|
+
next: (result: any) => {
|
|
257
|
+
const subscriptionResult: SubscriptionResult<TData> = {
|
|
258
|
+
data: result.data,
|
|
259
|
+
loading: false,
|
|
260
|
+
error: result.errors?.[0] as Error,
|
|
261
|
+
};
|
|
262
|
+
observer.next(subscriptionResult);
|
|
263
|
+
options?.onData?.(result.data);
|
|
264
|
+
},
|
|
265
|
+
error: (error: any) => {
|
|
266
|
+
const subscriptionResult: SubscriptionResult<TData> = {
|
|
267
|
+
data: undefined,
|
|
268
|
+
loading: false,
|
|
269
|
+
error,
|
|
270
|
+
};
|
|
271
|
+
observer.next(subscriptionResult);
|
|
272
|
+
options?.onError?.(error);
|
|
273
|
+
},
|
|
274
|
+
complete: () => {
|
|
275
|
+
observer.complete();
|
|
276
|
+
options?.onComplete?.();
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
return () => sub.unsubscribe();
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Query all table data - Adapted to API without store prefix
|
|
286
|
+
*
|
|
287
|
+
* OrderBy field name support:
|
|
288
|
+
* - camelCase: { field: 'updatedAt', direction: 'DESC' } → UPDATED_AT_DESC
|
|
289
|
+
* - snake_case: { field: 'updated_at', direction: 'DESC' } → UPDATED_AT_DESC
|
|
290
|
+
*
|
|
291
|
+
* Usage examples:
|
|
292
|
+
* ```ts
|
|
293
|
+
* // Using camelCase field names
|
|
294
|
+
* const result = await client.getAllTables('account', {
|
|
295
|
+
* orderBy: [{ field: 'updatedAt', direction: 'DESC' }]
|
|
296
|
+
* });
|
|
297
|
+
*
|
|
298
|
+
* // Using snake_case field names
|
|
299
|
+
* const result = await client.getAllTables('account', {
|
|
300
|
+
* orderBy: [{ field: 'updated_at', direction: 'DESC' }]
|
|
301
|
+
* });
|
|
302
|
+
*
|
|
303
|
+
* // Mixed usage
|
|
304
|
+
* const result = await client.getAllTables('account', {
|
|
305
|
+
* orderBy: [
|
|
306
|
+
* { field: 'updatedAt', direction: 'DESC' },
|
|
307
|
+
* { field: 'created_at', direction: 'ASC' }
|
|
308
|
+
* ]
|
|
309
|
+
* });
|
|
310
|
+
* ```
|
|
311
|
+
*/
|
|
312
|
+
async getAllTables<T extends StoreTableRow>(
|
|
313
|
+
tableName: string,
|
|
314
|
+
params?: BaseQueryParams & {
|
|
315
|
+
filter?: Record<string, any>;
|
|
316
|
+
orderBy?: OrderBy[];
|
|
317
|
+
fields?: string[]; // Allow users to specify fields to query, auto-parse from dubhe config if not specified
|
|
318
|
+
}
|
|
319
|
+
): Promise<Connection<T>> {
|
|
320
|
+
// Ensure using plural form of table name
|
|
321
|
+
const pluralTableName = this.getPluralTableName(tableName);
|
|
322
|
+
|
|
323
|
+
// Convert OrderBy to enum values
|
|
324
|
+
const orderByEnums = convertOrderByToEnum(params?.orderBy);
|
|
325
|
+
|
|
326
|
+
// Dynamically build query
|
|
327
|
+
const query = gql`
|
|
328
|
+
query GetAllTables(
|
|
329
|
+
$first: Int
|
|
330
|
+
$last: Int
|
|
331
|
+
$after: Cursor
|
|
332
|
+
$before: Cursor
|
|
333
|
+
$filter: ${this.getFilterTypeName(tableName)}
|
|
334
|
+
$orderBy: [${this.getOrderByTypeName(tableName)}!]
|
|
335
|
+
) {
|
|
336
|
+
${pluralTableName}(
|
|
337
|
+
first: $first
|
|
338
|
+
last: $last
|
|
339
|
+
after: $after
|
|
340
|
+
before: $before
|
|
341
|
+
filter: $filter
|
|
342
|
+
orderBy: $orderBy
|
|
343
|
+
) {
|
|
344
|
+
totalCount
|
|
345
|
+
pageInfo {
|
|
346
|
+
hasNextPage
|
|
347
|
+
hasPreviousPage
|
|
348
|
+
startCursor
|
|
349
|
+
endCursor
|
|
350
|
+
}
|
|
351
|
+
edges {
|
|
352
|
+
cursor
|
|
353
|
+
node {
|
|
354
|
+
${this.convertTableFields(tableName, params?.fields)}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
`;
|
|
360
|
+
|
|
361
|
+
// Build query parameters using enum values
|
|
362
|
+
const queryParams = {
|
|
363
|
+
first: params?.first,
|
|
364
|
+
last: params?.last,
|
|
365
|
+
after: params?.after,
|
|
366
|
+
before: params?.before,
|
|
367
|
+
filter: params?.filter,
|
|
368
|
+
orderBy: orderByEnums,
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// const result = await this.query(query, queryParams, {
|
|
372
|
+
// cachePolicy: 'no-cache',
|
|
373
|
+
// });
|
|
374
|
+
|
|
375
|
+
const result = await this.query(query, queryParams);
|
|
376
|
+
|
|
377
|
+
if (result.error) {
|
|
378
|
+
throw result.error;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return (
|
|
382
|
+
(result.data as any)?.[pluralTableName] || {
|
|
383
|
+
edges: [],
|
|
384
|
+
pageInfo: { hasNextPage: false, hasPreviousPage: false },
|
|
385
|
+
}
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Get single table record by condition - Adapted to API without store prefix
|
|
391
|
+
*/
|
|
392
|
+
async getTableByCondition<T extends StoreTableRow>(
|
|
393
|
+
tableName: string,
|
|
394
|
+
condition: Record<string, any>,
|
|
395
|
+
fields?: string[] // Allow users to specify fields to query
|
|
396
|
+
): Promise<T | null> {
|
|
397
|
+
// Build query field name, e.g.: accountByAssetIdAndAccount
|
|
398
|
+
const conditionKeys = Object.keys(condition);
|
|
399
|
+
|
|
400
|
+
// Use singular form of table name for single record query
|
|
401
|
+
const singularTableName = this.getSingularTableName(tableName);
|
|
402
|
+
|
|
403
|
+
const query = gql`
|
|
404
|
+
query GetTableByCondition(${conditionKeys.map((key, index) => `$${key}: String!`).join(', ')}) {
|
|
405
|
+
${singularTableName}(${conditionKeys.map((key) => `${key}: $${key}`).join(', ')}) {
|
|
406
|
+
${this.convertTableFields(tableName, fields)}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
`;
|
|
410
|
+
|
|
411
|
+
const result = await this.query(query, condition);
|
|
412
|
+
|
|
413
|
+
if (result.error) {
|
|
414
|
+
throw result.error;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return (result.data as any)?.[singularTableName] || null;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Subscribe to table data changes - Using PostGraphile's listen subscription feature
|
|
422
|
+
*/
|
|
423
|
+
subscribeToTableChanges<T extends StoreTableRow>(
|
|
424
|
+
tableName: string,
|
|
425
|
+
options?: SubscriptionOptions & {
|
|
426
|
+
fields?: string[]; // Allow users to specify fields to subscribe to
|
|
427
|
+
initialEvent?: boolean; // Whether to trigger initial event immediately
|
|
428
|
+
first?: number; // Limit the number of returned records
|
|
429
|
+
topicPrefix?: string; // Custom topic prefix, defaults to table name
|
|
430
|
+
}
|
|
431
|
+
): Observable<SubscriptionResult<{ listen: { query: any } }>> {
|
|
432
|
+
// PostGraphile automatically adds 'postgraphile:' prefix to all topics
|
|
433
|
+
// So here we use more concise topic naming
|
|
434
|
+
const topic = options?.topicPrefix
|
|
435
|
+
? `${options.topicPrefix}${tableName}`
|
|
436
|
+
: `store_${this.getSingularTableName(tableName)}`;
|
|
437
|
+
|
|
438
|
+
const pluralTableName = this.getPluralTableName(tableName); // Ensure using plural form
|
|
439
|
+
const fields = this.convertTableFields(tableName, options?.fields);
|
|
440
|
+
|
|
441
|
+
const subscription = gql`
|
|
442
|
+
subscription ListenToTableChanges($topic: String!, $initialEvent: Boolean) {
|
|
443
|
+
listen(topic: $topic, initialEvent: $initialEvent) {
|
|
444
|
+
query {
|
|
445
|
+
${pluralTableName}(first: ${options?.first || 10}, orderBy: UPDATED_AT_DESC) {
|
|
446
|
+
totalCount
|
|
447
|
+
nodes {
|
|
448
|
+
${fields}
|
|
449
|
+
}
|
|
450
|
+
pageInfo {
|
|
451
|
+
hasNextPage
|
|
452
|
+
endCursor
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
`;
|
|
459
|
+
|
|
460
|
+
return this.subscribe(
|
|
461
|
+
subscription,
|
|
462
|
+
{
|
|
463
|
+
topic,
|
|
464
|
+
initialEvent: options?.initialEvent || false,
|
|
465
|
+
},
|
|
466
|
+
options
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Advanced listen subscription - Support custom queries
|
|
472
|
+
*/
|
|
473
|
+
subscribeWithListen<T = any>(
|
|
474
|
+
topic: string,
|
|
475
|
+
query: string,
|
|
476
|
+
options?: SubscriptionOptions & {
|
|
477
|
+
initialEvent?: boolean;
|
|
478
|
+
variables?: Record<string, any>;
|
|
479
|
+
}
|
|
480
|
+
): Observable<SubscriptionResult<{ listen: { query: T } }>> {
|
|
481
|
+
const subscription = gql`
|
|
482
|
+
subscription CustomListenSubscription($topic: String!, $initialEvent: Boolean) {
|
|
483
|
+
listen(topic: $topic, initialEvent: $initialEvent) {
|
|
484
|
+
query {
|
|
485
|
+
${query}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
`;
|
|
490
|
+
|
|
491
|
+
return this.subscribe(
|
|
492
|
+
subscription,
|
|
493
|
+
{
|
|
494
|
+
topic,
|
|
495
|
+
initialEvent: options?.initialEvent || false,
|
|
496
|
+
...options?.variables,
|
|
497
|
+
},
|
|
498
|
+
options
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Subscribe to data changes with specific conditions
|
|
504
|
+
*/
|
|
505
|
+
subscribeToFilteredTableChanges<T extends StoreTableRow>(
|
|
506
|
+
tableName: string,
|
|
507
|
+
filter?: Record<string, any>,
|
|
508
|
+
options?: SubscriptionOptions & {
|
|
509
|
+
fields?: string[];
|
|
510
|
+
initialEvent?: boolean;
|
|
511
|
+
orderBy?: OrderBy[];
|
|
512
|
+
first?: number;
|
|
513
|
+
topicPrefix?: string; // Custom topic prefix
|
|
514
|
+
}
|
|
515
|
+
): Observable<SubscriptionResult<{ listen: { query: any } }>> {
|
|
516
|
+
// Improved topic naming, support custom prefix
|
|
517
|
+
const topic = options?.topicPrefix
|
|
518
|
+
? `${options.topicPrefix}${tableName}`
|
|
519
|
+
: `store_${this.getSingularTableName(tableName)}`;
|
|
520
|
+
|
|
521
|
+
const pluralTableName = this.getPluralTableName(tableName); // Ensure using plural form
|
|
522
|
+
const fields = this.convertTableFields(tableName, options?.fields);
|
|
523
|
+
const orderByEnum = convertOrderByToEnum(options?.orderBy);
|
|
524
|
+
const first = options?.first || 10;
|
|
525
|
+
|
|
526
|
+
const subscription = gql`
|
|
527
|
+
subscription FilteredListenSubscription(
|
|
528
|
+
$topic: String!,
|
|
529
|
+
$initialEvent: Boolean,
|
|
530
|
+
$filter: ${this.getFilterTypeName(tableName)},
|
|
531
|
+
$orderBy: [${this.getOrderByTypeName(tableName)}!],
|
|
532
|
+
$first: Int
|
|
533
|
+
) {
|
|
534
|
+
listen(topic: $topic, initialEvent: $initialEvent) {
|
|
535
|
+
query {
|
|
536
|
+
${pluralTableName}(
|
|
537
|
+
first: $first,
|
|
538
|
+
filter: $filter,
|
|
539
|
+
orderBy: $orderBy
|
|
540
|
+
) {
|
|
541
|
+
totalCount
|
|
542
|
+
nodes {
|
|
543
|
+
${fields}
|
|
544
|
+
}
|
|
545
|
+
pageInfo {
|
|
546
|
+
hasNextPage
|
|
547
|
+
endCursor
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
`;
|
|
554
|
+
|
|
555
|
+
return this.subscribe(
|
|
556
|
+
subscription,
|
|
557
|
+
{
|
|
558
|
+
topic,
|
|
559
|
+
initialEvent: options?.initialEvent || false,
|
|
560
|
+
filter,
|
|
561
|
+
orderBy: orderByEnum,
|
|
562
|
+
first,
|
|
563
|
+
},
|
|
564
|
+
options
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Subscribe to multiple table data changes - Support batch subscription of table name list
|
|
570
|
+
*/
|
|
571
|
+
subscribeToMultipleTables<T extends StoreTableRow>(
|
|
572
|
+
tableConfigs: MultiTableSubscriptionConfig[],
|
|
573
|
+
globalOptions?: SubscriptionOptions
|
|
574
|
+
): Observable<MultiTableSubscriptionData> {
|
|
575
|
+
return new Observable((observer: any) => {
|
|
576
|
+
const subscriptions: Array<{ tableName: string; subscription: any }> = [];
|
|
577
|
+
const latestData: MultiTableSubscriptionData = {};
|
|
578
|
+
|
|
579
|
+
// Create independent subscription for each table
|
|
580
|
+
tableConfigs.forEach(({ tableName, options }) => {
|
|
581
|
+
const subscription = this.subscribeToFilteredTableChanges<T>(
|
|
582
|
+
tableName,
|
|
583
|
+
options?.filter,
|
|
584
|
+
{
|
|
585
|
+
...options,
|
|
586
|
+
onData: (data) => {
|
|
587
|
+
// Update latest data for this table
|
|
588
|
+
latestData[tableName] = data;
|
|
589
|
+
|
|
590
|
+
// Call table-level callback
|
|
591
|
+
if (options?.onData) {
|
|
592
|
+
options.onData(data);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Call global callback
|
|
596
|
+
if (globalOptions?.onData) {
|
|
597
|
+
globalOptions.onData(latestData);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Send complete multi-table data
|
|
601
|
+
observer.next({ ...latestData });
|
|
602
|
+
},
|
|
603
|
+
onError: (error) => {
|
|
604
|
+
// Call table-level error callback
|
|
605
|
+
if (options?.onError) {
|
|
606
|
+
options.onError(error);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Call global error callback
|
|
610
|
+
if (globalOptions?.onError) {
|
|
611
|
+
globalOptions.onError(error);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Send error
|
|
615
|
+
observer.error(error);
|
|
616
|
+
},
|
|
617
|
+
}
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
subscriptions.push({ tableName, subscription });
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// Start all subscriptions
|
|
624
|
+
const activeSubscriptions = subscriptions.map(({ subscription }) =>
|
|
625
|
+
subscription.subscribe()
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
// Return cleanup function
|
|
629
|
+
return () => {
|
|
630
|
+
activeSubscriptions.forEach((sub) => sub.unsubscribe());
|
|
631
|
+
|
|
632
|
+
// Call completion callback
|
|
633
|
+
if (globalOptions?.onComplete) {
|
|
634
|
+
globalOptions.onComplete();
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Simplified multi-table subscription - Support table name array and unified configuration
|
|
642
|
+
*/
|
|
643
|
+
subscribeToTableList<T extends StoreTableRow>(
|
|
644
|
+
tableNames: string[],
|
|
645
|
+
options?: SubscriptionOptions & {
|
|
646
|
+
fields?: string[];
|
|
647
|
+
filter?: Record<string, any>;
|
|
648
|
+
initialEvent?: boolean;
|
|
649
|
+
first?: number;
|
|
650
|
+
topicPrefix?: string;
|
|
651
|
+
}
|
|
652
|
+
): Observable<MultiTableSubscriptionData> {
|
|
653
|
+
const tableConfigs: MultiTableSubscriptionConfig[] = tableNames.map(
|
|
654
|
+
(tableName) => ({
|
|
655
|
+
tableName,
|
|
656
|
+
options: {
|
|
657
|
+
...options,
|
|
658
|
+
// Use same configuration for each table
|
|
659
|
+
fields: options?.fields,
|
|
660
|
+
filter: options?.filter,
|
|
661
|
+
initialEvent: options?.initialEvent,
|
|
662
|
+
first: options?.first,
|
|
663
|
+
topicPrefix: options?.topicPrefix,
|
|
664
|
+
},
|
|
665
|
+
})
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
return this.subscribeToMultipleTables<T>(tableConfigs, options);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Build dynamic query - Adapted to API without store prefix
|
|
673
|
+
*/
|
|
674
|
+
buildQuery(
|
|
675
|
+
tableName: string,
|
|
676
|
+
fields: string[],
|
|
677
|
+
params?: {
|
|
678
|
+
filter?: Record<string, any>;
|
|
679
|
+
orderBy?: OrderBy[];
|
|
680
|
+
first?: number;
|
|
681
|
+
after?: string;
|
|
682
|
+
}
|
|
683
|
+
): TypedDocumentNode {
|
|
684
|
+
const pluralTableName = this.getPluralTableName(tableName); // Ensure using plural form
|
|
685
|
+
const fieldSelection = fields.join('\n ');
|
|
686
|
+
|
|
687
|
+
return gql`
|
|
688
|
+
query DynamicQuery(
|
|
689
|
+
$first: Int
|
|
690
|
+
$after: Cursor
|
|
691
|
+
$filter: ${this.getFilterTypeName(tableName)}
|
|
692
|
+
$orderBy: [${this.getOrderByTypeName(tableName)}!]
|
|
693
|
+
) {
|
|
694
|
+
${pluralTableName}(
|
|
695
|
+
first: $first
|
|
696
|
+
after: $after
|
|
697
|
+
filter: $filter
|
|
698
|
+
orderBy: $orderBy
|
|
699
|
+
) {
|
|
700
|
+
totalCount
|
|
701
|
+
pageInfo {
|
|
702
|
+
hasNextPage
|
|
703
|
+
endCursor
|
|
704
|
+
}
|
|
705
|
+
edges {
|
|
706
|
+
cursor
|
|
707
|
+
node {
|
|
708
|
+
${fieldSelection}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
`;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Batch query multiple tables - Adapted to API without store prefix
|
|
718
|
+
*/
|
|
719
|
+
async batchQuery<T extends Record<string, any>>(
|
|
720
|
+
queries: Array<{
|
|
721
|
+
key: string;
|
|
722
|
+
tableName: string;
|
|
723
|
+
params?: BaseQueryParams & {
|
|
724
|
+
filter?: Record<string, any>;
|
|
725
|
+
orderBy?: OrderBy[];
|
|
726
|
+
fields?: string[]; // Allow users to specify fields to query
|
|
727
|
+
};
|
|
728
|
+
}>
|
|
729
|
+
): Promise<Record<string, Connection<StoreTableRow>>> {
|
|
730
|
+
const batchPromises = queries.map(async ({ key, tableName, params }) => {
|
|
731
|
+
const result = await this.getAllTables(tableName, params);
|
|
732
|
+
return { key, result };
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
const results = await Promise.all(batchPromises);
|
|
736
|
+
|
|
737
|
+
return results.reduce(
|
|
738
|
+
(acc, { key, result }) => {
|
|
739
|
+
acc[key] = result;
|
|
740
|
+
return acc;
|
|
741
|
+
},
|
|
742
|
+
{} as Record<string, Connection<StoreTableRow>>
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Real-time data stream listener - Adapted to API without store prefix
|
|
748
|
+
*/
|
|
749
|
+
createRealTimeDataStream<T extends StoreTableRow>(
|
|
750
|
+
tableName: string,
|
|
751
|
+
initialQuery?: BaseQueryParams & { filter?: Record<string, any> }
|
|
752
|
+
): Observable<Connection<T>> {
|
|
753
|
+
return new Observable((observer: any) => {
|
|
754
|
+
// First execute initial query
|
|
755
|
+
this.getAllTables<T>(tableName, initialQuery)
|
|
756
|
+
.then((initialData) => {
|
|
757
|
+
observer.next(initialData);
|
|
758
|
+
})
|
|
759
|
+
.catch((error) => observer.error(error));
|
|
760
|
+
|
|
761
|
+
// Then subscribe to real-time updates
|
|
762
|
+
const subscription = this.subscribeToTableChanges<T>(tableName, {
|
|
763
|
+
onData: () => {
|
|
764
|
+
// When data changes, re-execute query
|
|
765
|
+
this.getAllTables<T>(tableName, initialQuery)
|
|
766
|
+
.then((updatedData) => {
|
|
767
|
+
observer.next(updatedData);
|
|
768
|
+
})
|
|
769
|
+
.catch((error) => observer.error(error));
|
|
770
|
+
},
|
|
771
|
+
onError: (error) => observer.error(error),
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
return () => subscription.subscribe().unsubscribe();
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Improved table name handling methods
|
|
779
|
+
private getFilterTypeName(tableName: string): string {
|
|
780
|
+
// Convert to singular form and apply PascalCase conversion
|
|
781
|
+
const singularName = this.getSingularTableName(tableName);
|
|
782
|
+
const pascalCaseName = this.toPascalCase(singularName);
|
|
783
|
+
|
|
784
|
+
// If already starts with Store, don't add Store prefix again
|
|
785
|
+
if (pascalCaseName.startsWith('Store')) {
|
|
786
|
+
return `${pascalCaseName}Filter`;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return `Store${pascalCaseName}Filter`;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
private getOrderByTypeName(tableName: string): string {
|
|
793
|
+
// Convert to plural form and apply PascalCase conversion
|
|
794
|
+
const pluralName = this.getPluralTableName(tableName);
|
|
795
|
+
const pascalCaseName = this.toPascalCase(pluralName);
|
|
796
|
+
|
|
797
|
+
// If already starts with Store, don't add Store prefix again
|
|
798
|
+
if (pascalCaseName.startsWith('Store')) {
|
|
799
|
+
return `${pascalCaseName}OrderBy`;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
return `Store${pascalCaseName}OrderBy`;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Convert singular table name to plural form (using pluralize library for correctness)
|
|
807
|
+
*/
|
|
808
|
+
private getPluralTableName(tableName: string): string {
|
|
809
|
+
// First convert to camelCase
|
|
810
|
+
const camelCaseName = this.toCamelCase(tableName);
|
|
811
|
+
|
|
812
|
+
// Use pluralize library for pluralization
|
|
813
|
+
return pluralize.plural(camelCaseName);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Convert plural table name to singular form (using pluralize library for correctness)
|
|
818
|
+
*/
|
|
819
|
+
private getSingularTableName(tableName: string): string {
|
|
820
|
+
// First convert to camelCase
|
|
821
|
+
const camelCaseName = this.toCamelCase(tableName);
|
|
822
|
+
|
|
823
|
+
// Use pluralize library for singularization
|
|
824
|
+
return pluralize.singular(camelCaseName);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Convert snake_case to camelCase
|
|
829
|
+
*/
|
|
830
|
+
private toCamelCase(str: string): string {
|
|
831
|
+
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Convert snake_case to PascalCase
|
|
836
|
+
*/
|
|
837
|
+
private toPascalCase(str: string): string {
|
|
838
|
+
const camelCase = this.toCamelCase(str);
|
|
839
|
+
return camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Convert camelCase or snake_case to SNAKE_CASE (for GraphQL enum values)
|
|
844
|
+
* Example: updatedAt -> UPDATED_AT, updated_at -> UPDATED_AT
|
|
845
|
+
*/
|
|
846
|
+
private toSnakeCase(str: string): string {
|
|
847
|
+
// If already snake_case, convert to uppercase directly
|
|
848
|
+
if (str.includes('_')) {
|
|
849
|
+
return str.toUpperCase();
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// If camelCase, first convert to snake_case then uppercase
|
|
853
|
+
return str
|
|
854
|
+
.replace(/([A-Z])/g, '_$1') // Add underscore before uppercase letters
|
|
855
|
+
.toLowerCase() // Convert to lowercase
|
|
856
|
+
.replace(/^_/, '') // Remove leading underscore
|
|
857
|
+
.toUpperCase(); // Convert to uppercase
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// private buildSingleQueryName(
|
|
861
|
+
// tableName: string,
|
|
862
|
+
// conditionKeys: string[]
|
|
863
|
+
// ): string {
|
|
864
|
+
// // Use camelCase conversion
|
|
865
|
+
// const camelCaseTableName = this.toCamelCase(tableName);
|
|
866
|
+
// const capitalizedKeys = conditionKeys.map(
|
|
867
|
+
// (key) => key.charAt(0).toUpperCase() + key.slice(1)
|
|
868
|
+
// );
|
|
869
|
+
// return `${camelCaseTableName}By${capitalizedKeys.join('And')}`;
|
|
870
|
+
// }
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Clear Apollo Client cache
|
|
874
|
+
*/
|
|
875
|
+
async clearCache(): Promise<void> {
|
|
876
|
+
await this.apolloClient.clearStore();
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Reset Apollo Client cache
|
|
881
|
+
*/
|
|
882
|
+
async resetCache(): Promise<void> {
|
|
883
|
+
await this.apolloClient.resetStore();
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Get Apollo Client instance (for advanced usage)
|
|
888
|
+
*/
|
|
889
|
+
getApolloClient(): ApolloClient<NormalizedCacheObject> {
|
|
890
|
+
return this.apolloClient;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Close client connection
|
|
895
|
+
*/
|
|
896
|
+
close(): void {
|
|
897
|
+
if (this.subscriptionClient) {
|
|
898
|
+
this.subscriptionClient.dispose();
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Get Dubhe metadata
|
|
904
|
+
*/
|
|
905
|
+
getDubheMetadata(): DubheMetadata | undefined {
|
|
906
|
+
return this.dubheMetadata;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Build dynamic cache field configuration
|
|
911
|
+
*/
|
|
912
|
+
private buildCacheFields(
|
|
913
|
+
cacheConfig?: DubheClientConfig['cacheConfig']
|
|
914
|
+
): Record<string, any> {
|
|
915
|
+
const fields: Record<string, any> = {};
|
|
916
|
+
|
|
917
|
+
// If no configuration, return empty field configuration
|
|
918
|
+
if (!cacheConfig) {
|
|
919
|
+
return fields;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Create pagination cache strategy for each configured table
|
|
923
|
+
if (cacheConfig.paginatedTables) {
|
|
924
|
+
cacheConfig.paginatedTables.forEach((tableName) => {
|
|
925
|
+
// Ensure using plural form of table name
|
|
926
|
+
const pluralTableName = this.getPluralTableName(tableName);
|
|
927
|
+
|
|
928
|
+
// Check if there's a custom merge strategy
|
|
929
|
+
const customStrategy =
|
|
930
|
+
cacheConfig.customMergeStrategies?.[pluralTableName];
|
|
931
|
+
|
|
932
|
+
fields[pluralTableName] = {
|
|
933
|
+
keyArgs: customStrategy?.keyArgs || ['filter', 'orderBy'],
|
|
934
|
+
merge: customStrategy?.merge || this.defaultMergeStrategy,
|
|
935
|
+
};
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Apply custom merge strategies (if any)
|
|
940
|
+
if (cacheConfig.customMergeStrategies) {
|
|
941
|
+
Object.entries(cacheConfig.customMergeStrategies).forEach(
|
|
942
|
+
([tableName, strategy]) => {
|
|
943
|
+
// If table name hasn't been configured yet, add it
|
|
944
|
+
if (!fields[tableName]) {
|
|
945
|
+
fields[tableName] = {
|
|
946
|
+
keyArgs: strategy.keyArgs || ['filter', 'orderBy'],
|
|
947
|
+
merge: strategy.merge || this.defaultMergeStrategy,
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
return fields;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Default pagination merge strategy
|
|
959
|
+
*/
|
|
960
|
+
private defaultMergeStrategy(existing = { edges: [] }, incoming: any) {
|
|
961
|
+
// Safety check, ensure incoming has edges property
|
|
962
|
+
if (!incoming || !Array.isArray(incoming.edges)) {
|
|
963
|
+
return existing;
|
|
964
|
+
}
|
|
965
|
+
return {
|
|
966
|
+
...incoming,
|
|
967
|
+
edges: [...(existing.edges || []), ...incoming.edges],
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Parse table information from dubhe metadata
|
|
973
|
+
*/
|
|
974
|
+
private parseTableInfoFromConfig(): void {
|
|
975
|
+
if (!this.dubheMetadata) {
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const { components = [], resources = [], enums = [] } = this.dubheMetadata;
|
|
980
|
+
|
|
981
|
+
// Process components array
|
|
982
|
+
components.forEach((componentObj: any) => {
|
|
983
|
+
Object.entries(componentObj).forEach(
|
|
984
|
+
([componentName, componentData]: [string, any]) => {
|
|
985
|
+
this.processTableData(componentName, componentData, enums);
|
|
986
|
+
}
|
|
987
|
+
);
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
// Process resources array
|
|
991
|
+
resources.forEach((resourceObj: any) => {
|
|
992
|
+
Object.entries(resourceObj).forEach(
|
|
993
|
+
([resourceName, resourceData]: [string, any]) => {
|
|
994
|
+
this.processTableData(resourceName, resourceData, enums);
|
|
995
|
+
}
|
|
996
|
+
);
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Process data for a single table
|
|
1002
|
+
*/
|
|
1003
|
+
private processTableData(
|
|
1004
|
+
tableName: string,
|
|
1005
|
+
tableData: any,
|
|
1006
|
+
enums: any[]
|
|
1007
|
+
): void {
|
|
1008
|
+
const snakeTableName = this.toSnakeCase(tableName);
|
|
1009
|
+
const fields: string[] = [];
|
|
1010
|
+
const enumFields: Record<string, string[]> = {};
|
|
1011
|
+
|
|
1012
|
+
// Process fields array
|
|
1013
|
+
if (tableData.fields && Array.isArray(tableData.fields)) {
|
|
1014
|
+
tableData.fields.forEach((fieldObj: any) => {
|
|
1015
|
+
Object.entries(fieldObj).forEach(
|
|
1016
|
+
([fieldName, fieldType]: [string, any]) => {
|
|
1017
|
+
const fieldNameCamelCase = this.toCamelCase(fieldName);
|
|
1018
|
+
fields.push(fieldNameCamelCase);
|
|
1019
|
+
// Check if it's an enum type
|
|
1020
|
+
// const typeStr = String(fieldType);
|
|
1021
|
+
// if (enums.length > 0) {
|
|
1022
|
+
// // Process enum types as needed here
|
|
1023
|
+
// // enumFields[fieldNameCamelCase] = [...];
|
|
1024
|
+
// }
|
|
1025
|
+
}
|
|
1026
|
+
);
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Add system fields
|
|
1031
|
+
fields.push('createdAt', 'updatedAt');
|
|
1032
|
+
|
|
1033
|
+
// Process primary keys
|
|
1034
|
+
const primaryKeys: string[] = tableData.keys.map((key: string) =>
|
|
1035
|
+
this.toCamelCase(key)
|
|
1036
|
+
);
|
|
1037
|
+
|
|
1038
|
+
const tableInfo: ParsedTableInfo = {
|
|
1039
|
+
tableName: snakeTableName,
|
|
1040
|
+
fields: [...new Set(fields)], // Remove duplicates
|
|
1041
|
+
primaryKeys,
|
|
1042
|
+
enumFields,
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
this.parsedTables.set(snakeTableName, tableInfo);
|
|
1046
|
+
this.parsedTables.set(this.toCamelCase(snakeTableName), tableInfo);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* Get table field information
|
|
1051
|
+
*/
|
|
1052
|
+
getTableFields(tableName: string): string[] {
|
|
1053
|
+
// Use getMinimalFields directly for clearer logic
|
|
1054
|
+
return this.getMinimalFields(tableName);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* Get table primary key information
|
|
1059
|
+
*/
|
|
1060
|
+
getTablePrimaryKeys(tableName: string): string[] {
|
|
1061
|
+
const tableInfo =
|
|
1062
|
+
this.parsedTables.get(tableName) ||
|
|
1063
|
+
this.parsedTables.get(this.toSnakeCase(tableName));
|
|
1064
|
+
return tableInfo?.primaryKeys || [];
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Get table enum field information
|
|
1069
|
+
*/
|
|
1070
|
+
getTableEnumFields(tableName: string): Record<string, string[]> {
|
|
1071
|
+
const tableInfo =
|
|
1072
|
+
this.parsedTables.get(tableName) ||
|
|
1073
|
+
this.parsedTables.get(this.toSnakeCase(tableName));
|
|
1074
|
+
return tableInfo?.enumFields || {};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/**
|
|
1078
|
+
* Get all parsed table information
|
|
1079
|
+
*/
|
|
1080
|
+
getAllTableInfo(): Map<string, ParsedTableInfo> {
|
|
1081
|
+
return new Map(this.parsedTables);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* Get table's minimal field set (for fallback)
|
|
1086
|
+
*/
|
|
1087
|
+
getMinimalFields(tableName: string): string[] {
|
|
1088
|
+
// If there's configuration, use fields from configuration
|
|
1089
|
+
const tableInfo =
|
|
1090
|
+
this.parsedTables.get(tableName) ||
|
|
1091
|
+
this.parsedTables.get(this.toSnakeCase(tableName));
|
|
1092
|
+
|
|
1093
|
+
if (tableInfo) {
|
|
1094
|
+
return tableInfo.fields;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
return ['createdAt', 'updatedAt'];
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Convert table fields to GraphQL query string
|
|
1102
|
+
*/
|
|
1103
|
+
private convertTableFields(
|
|
1104
|
+
tableName: string,
|
|
1105
|
+
customFields?: string[]
|
|
1106
|
+
): string {
|
|
1107
|
+
let fields: string[];
|
|
1108
|
+
|
|
1109
|
+
if (customFields && customFields.length > 0) {
|
|
1110
|
+
fields = customFields;
|
|
1111
|
+
} else {
|
|
1112
|
+
// Try to get fields from dubhe configuration
|
|
1113
|
+
const autoFields = this.getTableFields(tableName);
|
|
1114
|
+
if (autoFields.length > 0) {
|
|
1115
|
+
fields = autoFields;
|
|
1116
|
+
} else {
|
|
1117
|
+
fields = ['createdAt', 'updatedAt'];
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// Field resolution debug logging disabled for cleaner output
|
|
1122
|
+
|
|
1123
|
+
return fields.join('\n ');
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// Export convenience function
|
|
1128
|
+
export function createDubheGraphqlClient(
|
|
1129
|
+
config: DubheClientConfig
|
|
1130
|
+
): DubheGraphqlClient {
|
|
1131
|
+
return new DubheGraphqlClient(config);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// Export common GraphQL query builders
|
|
1135
|
+
export const QueryBuilders = {
|
|
1136
|
+
// Build basic query - Adapted to API without store prefix
|
|
1137
|
+
basic: (
|
|
1138
|
+
tableName: string,
|
|
1139
|
+
fields: string[] = ['createdAt', 'updatedAt']
|
|
1140
|
+
) => gql`
|
|
1141
|
+
query Basic${tableName.charAt(0).toUpperCase() + tableName.slice(1)}Query(
|
|
1142
|
+
$first: Int
|
|
1143
|
+
$after: String
|
|
1144
|
+
$filter: ${tableName.charAt(0).toUpperCase() + tableName.slice(1)}Filter
|
|
1145
|
+
) {
|
|
1146
|
+
${tableName}(first: $first, after: $after, filter: $filter) {
|
|
1147
|
+
totalCount
|
|
1148
|
+
pageInfo {
|
|
1149
|
+
hasNextPage
|
|
1150
|
+
endCursor
|
|
1151
|
+
}
|
|
1152
|
+
edges {
|
|
1153
|
+
cursor
|
|
1154
|
+
node {
|
|
1155
|
+
${fields.join('\n ')}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
`,
|
|
1161
|
+
|
|
1162
|
+
// Build subscription query - Adapted to API without store prefix
|
|
1163
|
+
subscription: (tableName: string) => gql`
|
|
1164
|
+
subscription ${tableName.charAt(0).toUpperCase() + tableName.slice(1)}Subscription {
|
|
1165
|
+
${tableName.charAt(0).toLowerCase() + tableName.slice(1)}Changed {
|
|
1166
|
+
createdAt
|
|
1167
|
+
updatedAt
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
`,
|
|
1171
|
+
};
|
|
1172
|
+
|
|
1173
|
+
/**
|
|
1174
|
+
* Helper function: Convert OrderBy format
|
|
1175
|
+
* Support camelCase and snake_case field names conversion to GraphQL enum values
|
|
1176
|
+
* Example: updatedAt -> UPDATED_AT_ASC, updated_at -> UPDATED_AT_ASC
|
|
1177
|
+
*/
|
|
1178
|
+
function convertOrderByToEnum(orderBy?: OrderBy[]): string[] {
|
|
1179
|
+
if (!orderBy || orderBy.length === 0) {
|
|
1180
|
+
return ['NATURAL'];
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
return orderBy.map((order) => {
|
|
1184
|
+
// Use unified conversion function to handle field names
|
|
1185
|
+
const field = toSnakeCaseForEnum(order.field);
|
|
1186
|
+
const direction = order.direction === 'DESC' ? 'DESC' : 'ASC';
|
|
1187
|
+
|
|
1188
|
+
// Combine field name and direction into enum value
|
|
1189
|
+
return `${field}_${direction}`;
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/**
|
|
1194
|
+
* Convert camelCase or snake_case to SNAKE_CASE (for GraphQL enum values)
|
|
1195
|
+
* Example: updatedAt -> UPDATED_AT, updated_at -> UPDATED_AT
|
|
1196
|
+
*/
|
|
1197
|
+
function toSnakeCaseForEnum(str: string): string {
|
|
1198
|
+
// If already snake_case, convert to uppercase directly
|
|
1199
|
+
if (str.includes('_')) {
|
|
1200
|
+
return str.toUpperCase();
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// If camelCase, first convert to snake_case then uppercase
|
|
1204
|
+
return str
|
|
1205
|
+
.replace(/([A-Z])/g, '_$1') // Add underscore before uppercase letters
|
|
1206
|
+
.toLowerCase() // Convert to lowercase
|
|
1207
|
+
.replace(/^_/, '') // Remove leading underscore
|
|
1208
|
+
.toUpperCase(); // Convert to uppercase
|
|
1209
|
+
}
|