@0xobelisk/graphql-server 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/Dockerfile +31 -0
- package/EXPRESS_MIGRATION.md +176 -0
- package/LICENSE +92 -0
- package/README.md +908 -0
- package/dist/config/subscription-config.d.ts +47 -0
- package/dist/config/subscription-config.d.ts.map +1 -0
- package/dist/config/subscription-config.js +133 -0
- package/dist/config/subscription-config.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +217 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/all-fields-filter-plugin.d.ts +4 -0
- package/dist/plugins/all-fields-filter-plugin.d.ts.map +1 -0
- package/dist/plugins/all-fields-filter-plugin.js +132 -0
- package/dist/plugins/all-fields-filter-plugin.js.map +1 -0
- package/dist/plugins/database-introspector.d.ts +23 -0
- package/dist/plugins/database-introspector.d.ts.map +1 -0
- package/dist/plugins/database-introspector.js +96 -0
- package/dist/plugins/database-introspector.js.map +1 -0
- package/dist/plugins/enhanced-playground.d.ts +9 -0
- package/dist/plugins/enhanced-playground.d.ts.map +1 -0
- package/dist/plugins/enhanced-playground.js +97 -0
- package/dist/plugins/enhanced-playground.js.map +1 -0
- package/dist/plugins/enhanced-server-manager.d.ts +28 -0
- package/dist/plugins/enhanced-server-manager.d.ts.map +1 -0
- package/dist/plugins/enhanced-server-manager.js +232 -0
- package/dist/plugins/enhanced-server-manager.js.map +1 -0
- package/dist/plugins/index.d.ts +9 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +26 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/plugins/postgraphile-config.d.ts +94 -0
- package/dist/plugins/postgraphile-config.d.ts.map +1 -0
- package/dist/plugins/postgraphile-config.js +183 -0
- package/dist/plugins/postgraphile-config.js.map +1 -0
- package/dist/plugins/query-filter.d.ts +4 -0
- package/dist/plugins/query-filter.d.ts.map +1 -0
- package/dist/plugins/query-filter.js +42 -0
- package/dist/plugins/query-filter.js.map +1 -0
- package/dist/plugins/simple-naming.d.ts +4 -0
- package/dist/plugins/simple-naming.d.ts.map +1 -0
- package/dist/plugins/simple-naming.js +79 -0
- package/dist/plugins/simple-naming.js.map +1 -0
- package/dist/plugins/welcome-page.d.ts +11 -0
- package/dist/plugins/welcome-page.d.ts.map +1 -0
- package/dist/plugins/welcome-page.js +203 -0
- package/dist/plugins/welcome-page.js.map +1 -0
- package/dist/universal-subscriptions.d.ts +32 -0
- package/dist/universal-subscriptions.d.ts.map +1 -0
- package/dist/universal-subscriptions.js +318 -0
- package/dist/universal-subscriptions.js.map +1 -0
- package/dist/utils/logger/index.d.ts +80 -0
- package/dist/utils/logger/index.d.ts.map +1 -0
- package/dist/utils/logger/index.js +232 -0
- package/dist/utils/logger/index.js.map +1 -0
- package/docker-compose.yml +87 -0
- package/package.json +71 -0
- package/server.log +62 -0
- package/src/config/subscription-config.ts +186 -0
- package/src/index.ts +239 -0
- package/src/plugins/README.md +123 -0
- package/src/plugins/all-fields-filter-plugin.ts +158 -0
- package/src/plugins/database-introspector.ts +126 -0
- package/src/plugins/enhanced-playground.ts +105 -0
- package/src/plugins/enhanced-server-manager.ts +282 -0
- package/src/plugins/index.ts +9 -0
- package/src/plugins/postgraphile-config.ts +226 -0
- package/src/plugins/query-filter.ts +50 -0
- package/src/plugins/simple-naming.ts +105 -0
- package/src/plugins/welcome-page.ts +218 -0
- package/src/universal-subscriptions.ts +397 -0
- package/src/utils/logger/README.md +193 -0
- package/src/utils/logger/index.ts +315 -0
- package/sui-indexer-schema.graphql +1004 -0
- package/test-express.js +124 -0
- package/test_listen_subscription.js +121 -0
- package/test_notification.js +63 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
// Express server manager - using Express framework and PostgreSQL subscriptions
|
|
2
|
+
|
|
3
|
+
import express, { Express, Request, Response } from 'express';
|
|
4
|
+
import { createServer, Server as HttpServer } from 'http';
|
|
5
|
+
import cors from 'cors';
|
|
6
|
+
import { Pool } from 'pg';
|
|
7
|
+
import { enhanceHttpServerWithSubscriptions } from 'postgraphile';
|
|
8
|
+
import { subscriptionConfig, SubscriptionConfig } from '../config/subscription-config';
|
|
9
|
+
import { systemLogger, serverLogger, logExpress } from '../utils/logger';
|
|
10
|
+
import { createWelcomePage, WelcomePageConfig } from './welcome-page';
|
|
11
|
+
import { createPlaygroundHtml, PostGraphileConfigOptions } from './postgraphile-config';
|
|
12
|
+
import type { DynamicTable } from './database-introspector';
|
|
13
|
+
|
|
14
|
+
export interface EnhancedServerConfig {
|
|
15
|
+
postgraphileMiddleware: any;
|
|
16
|
+
pgPool: Pool;
|
|
17
|
+
tableNames: string[];
|
|
18
|
+
databaseUrl: string;
|
|
19
|
+
allTables: DynamicTable[];
|
|
20
|
+
welcomeConfig: WelcomePageConfig;
|
|
21
|
+
postgraphileConfigOptions: PostGraphileConfigOptions;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class EnhancedServerManager {
|
|
25
|
+
private config: SubscriptionConfig;
|
|
26
|
+
private app: Express | null = null;
|
|
27
|
+
private httpServer: HttpServer | null = null;
|
|
28
|
+
|
|
29
|
+
constructor() {
|
|
30
|
+
this.config = subscriptionConfig.getConfig();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Create Express application
|
|
34
|
+
private createExpressApp(serverConfig: EnhancedServerConfig): Express {
|
|
35
|
+
const { postgraphileMiddleware, allTables, welcomeConfig, postgraphileConfigOptions } =
|
|
36
|
+
serverConfig;
|
|
37
|
+
|
|
38
|
+
const app = express();
|
|
39
|
+
|
|
40
|
+
// Middleware configuration
|
|
41
|
+
app.use(
|
|
42
|
+
cors({
|
|
43
|
+
origin: '*',
|
|
44
|
+
methods: ['GET', 'POST', 'OPTIONS'],
|
|
45
|
+
allowedHeaders: ['Content-Type', 'Authorization']
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Request logging middleware
|
|
50
|
+
app.use((req: Request, res: Response, next) => {
|
|
51
|
+
const startTime = Date.now();
|
|
52
|
+
|
|
53
|
+
res.on('finish', () => {
|
|
54
|
+
logExpress(req.method, req.path, res.statusCode, startTime, {
|
|
55
|
+
userAgent: req.get('user-agent')?.substring(0, 50)
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
next();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Route configuration
|
|
63
|
+
|
|
64
|
+
// Root path - welcome page
|
|
65
|
+
app.get('/', (req: Request, res: Response) => {
|
|
66
|
+
res.set('Content-Type', 'text/html; charset=utf-8');
|
|
67
|
+
res.send(createWelcomePage(allTables, welcomeConfig));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// GraphQL Playground
|
|
71
|
+
app.get('/playground', (req: Request, res: Response) => {
|
|
72
|
+
res.set('Content-Type', 'text/html; charset=utf-8');
|
|
73
|
+
res.send(createPlaygroundHtml(postgraphileConfigOptions));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Redirect old GraphiQL paths
|
|
77
|
+
app.get('/graphiql*', (req: Request, res: Response) => {
|
|
78
|
+
serverLogger.info('Redirecting old GraphiQL path', {
|
|
79
|
+
from: req.path,
|
|
80
|
+
to: '/playground'
|
|
81
|
+
});
|
|
82
|
+
res.redirect(301, '/playground');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Health check endpoint
|
|
86
|
+
app.get('/health', (req: Request, res: Response) => {
|
|
87
|
+
res.json({
|
|
88
|
+
status: 'healthy',
|
|
89
|
+
subscriptions: this.getSubscriptionStatus(),
|
|
90
|
+
timestamp: new Date().toISOString()
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Subscription configuration endpoint
|
|
95
|
+
app.get('/subscription-config', (req: Request, res: Response) => {
|
|
96
|
+
res.json(subscriptionConfig.generateClientConfig());
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Configuration documentation endpoint
|
|
100
|
+
app.get('/subscription-docs', (req: Request, res: Response) => {
|
|
101
|
+
res.set('Content-Type', 'text/plain');
|
|
102
|
+
res.send(subscriptionConfig.generateDocumentation());
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// PostGraphile middleware - mount at root path, let PostGraphile handle routing itself
|
|
106
|
+
app.use((req: Request, res: Response, next) => {
|
|
107
|
+
// Check if PostGraphile middleware exists
|
|
108
|
+
if (!postgraphileMiddleware) {
|
|
109
|
+
console.error('❌ PostGraphile middleware is null!');
|
|
110
|
+
if (req.path.startsWith('/graphql')) {
|
|
111
|
+
res.status(500).json({
|
|
112
|
+
error: 'PostGraphile middleware not properly initialized'
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
next();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
postgraphileMiddleware(req, res, next);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error('❌ PostGraphile middleware execution error:', error);
|
|
124
|
+
if (req.path.startsWith('/graphql')) {
|
|
125
|
+
res.status(500).json({
|
|
126
|
+
error: 'PostGraphile execution error',
|
|
127
|
+
details: error instanceof Error ? error.message : String(error)
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
next();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Error handling middleware
|
|
136
|
+
app.use((err: Error, req: Request, res: Response, next: express.NextFunction) => {
|
|
137
|
+
serverLogger.error('Express error handling', err, {
|
|
138
|
+
url: req.originalUrl,
|
|
139
|
+
method: req.method,
|
|
140
|
+
userAgent: req.get('user-agent')?.substring(0, 50)
|
|
141
|
+
});
|
|
142
|
+
res.status(500).send('Internal Server Error');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return app;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Create and configure HTTP server
|
|
149
|
+
async createEnhancedServer(serverConfig: EnhancedServerConfig): Promise<HttpServer> {
|
|
150
|
+
const { postgraphileMiddleware } = serverConfig;
|
|
151
|
+
|
|
152
|
+
// Create Express application
|
|
153
|
+
this.app = this.createExpressApp(serverConfig);
|
|
154
|
+
|
|
155
|
+
// Create HTTP server
|
|
156
|
+
this.httpServer = createServer(this.app);
|
|
157
|
+
|
|
158
|
+
// Enable PostgreSQL subscriptions and WebSocket support
|
|
159
|
+
if (this.config.capabilities.pgSubscriptions) {
|
|
160
|
+
enhanceHttpServerWithSubscriptions(this.httpServer, postgraphileMiddleware, {
|
|
161
|
+
// Enable WebSocket transport
|
|
162
|
+
graphqlRoute: '/graphql'
|
|
163
|
+
});
|
|
164
|
+
systemLogger.info('✅ PostgreSQL subscriptions and WebSocket enabled', {
|
|
165
|
+
pgSubscriptions: this.config.capabilities.pgSubscriptions,
|
|
166
|
+
webSocket: true
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
serverLogger.info('🚀 Express server creation completed', {
|
|
171
|
+
framework: 'Express',
|
|
172
|
+
graphqlPort: this.config.graphqlPort,
|
|
173
|
+
capabilities: {
|
|
174
|
+
pgSubscriptions: this.config.capabilities.pgSubscriptions
|
|
175
|
+
},
|
|
176
|
+
recommendedMethod: 'pg-subscriptions'
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return this.httpServer;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Start server
|
|
183
|
+
async startServer(): Promise<void> {
|
|
184
|
+
if (!this.httpServer) {
|
|
185
|
+
throw new Error('Server not created, please call createEnhancedServer() first');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return new Promise((resolve, reject) => {
|
|
189
|
+
this.httpServer!.listen(this.config.graphqlPort, (err?: Error) => {
|
|
190
|
+
if (err) {
|
|
191
|
+
reject(err);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.logServerStatus();
|
|
196
|
+
resolve();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Log server status
|
|
202
|
+
private logServerStatus() {
|
|
203
|
+
const clientConfig = subscriptionConfig.generateClientConfig();
|
|
204
|
+
|
|
205
|
+
serverLogger.info('🎉 Express GraphQL server started successfully!', {
|
|
206
|
+
port: this.config.graphqlPort,
|
|
207
|
+
framework: 'Express',
|
|
208
|
+
endpoints: {
|
|
209
|
+
home: `http://localhost:${this.config.graphqlPort}/`,
|
|
210
|
+
playground: `http://localhost:${this.config.graphqlPort}/playground`,
|
|
211
|
+
graphql: clientConfig.graphqlEndpoint,
|
|
212
|
+
subscription: clientConfig.subscriptionEndpoint,
|
|
213
|
+
health: `http://localhost:${this.config.graphqlPort}/health`,
|
|
214
|
+
config: `http://localhost:${this.config.graphqlPort}/subscription-config`,
|
|
215
|
+
docs: `http://localhost:${this.config.graphqlPort}/subscription-docs`
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Display main access links
|
|
220
|
+
console.log('\n' + '🌟'.repeat(30));
|
|
221
|
+
console.log('🏠 Homepage: ' + `http://localhost:${this.config.graphqlPort}/`);
|
|
222
|
+
console.log('🎮 Playground: ' + `http://localhost:${this.config.graphqlPort}/playground`);
|
|
223
|
+
console.log('🔗 GraphQL: ' + clientConfig.graphqlEndpoint);
|
|
224
|
+
console.log('📡 WebSocket: ' + clientConfig.subscriptionEndpoint);
|
|
225
|
+
console.log('🌟'.repeat(30) + '\n');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Get subscription status
|
|
229
|
+
private getSubscriptionStatus() {
|
|
230
|
+
return {
|
|
231
|
+
enabled: this.config.capabilities.pgSubscriptions,
|
|
232
|
+
method: 'pg-subscriptions',
|
|
233
|
+
config: subscriptionConfig.generateClientConfig()
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Quick shutdown
|
|
238
|
+
async quickShutdown(): Promise<void> {
|
|
239
|
+
systemLogger.info('🛑 Starting quick shutdown of Express server...');
|
|
240
|
+
|
|
241
|
+
if (this.httpServer) {
|
|
242
|
+
this.httpServer.close();
|
|
243
|
+
systemLogger.info('✅ HTTP server closed');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
systemLogger.info('🎯 Express server quick shutdown completed');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Graceful shutdown
|
|
250
|
+
async gracefulShutdown(pgPool: Pool): Promise<void> {
|
|
251
|
+
systemLogger.info('🛑 Starting graceful shutdown of Express server...');
|
|
252
|
+
|
|
253
|
+
const shutdownPromises: Promise<void>[] = [];
|
|
254
|
+
|
|
255
|
+
// Close HTTP server
|
|
256
|
+
if (this.httpServer) {
|
|
257
|
+
shutdownPromises.push(
|
|
258
|
+
new Promise((resolve) => {
|
|
259
|
+
this.httpServer!.close(() => {
|
|
260
|
+
systemLogger.info('✅ HTTP server closed');
|
|
261
|
+
resolve();
|
|
262
|
+
});
|
|
263
|
+
})
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Close database connection pool
|
|
268
|
+
shutdownPromises.push(
|
|
269
|
+
pgPool.end().then(() => {
|
|
270
|
+
systemLogger.info('✅ Database connection pool closed');
|
|
271
|
+
})
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
await Promise.all(shutdownPromises);
|
|
276
|
+
systemLogger.info('🎯 Express server graceful shutdown completed');
|
|
277
|
+
} catch (error) {
|
|
278
|
+
systemLogger.error('❌ Error occurred during shutdown process', error);
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// 插件统一导出入口
|
|
2
|
+
export * from './database-introspector';
|
|
3
|
+
export * from './welcome-page';
|
|
4
|
+
export * from './postgraphile-config';
|
|
5
|
+
export * from './query-filter';
|
|
6
|
+
export * from './simple-naming';
|
|
7
|
+
export * from './all-fields-filter-plugin';
|
|
8
|
+
export * from './enhanced-server-manager';
|
|
9
|
+
export * from './enhanced-playground';
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { QueryFilterPlugin } from './query-filter';
|
|
2
|
+
import { SimpleNamingPlugin } from './simple-naming';
|
|
3
|
+
import { AllFieldsFilterPlugin } from './all-fields-filter-plugin';
|
|
4
|
+
import { createEnhancedPlayground } from './enhanced-playground';
|
|
5
|
+
import ConnectionFilterPlugin from 'postgraphile-plugin-connection-filter';
|
|
6
|
+
import PgSimplifyInflectorPlugin from '@graphile-contrib/pg-simplify-inflector';
|
|
7
|
+
import { makePluginHook } from 'postgraphile';
|
|
8
|
+
import PgPubSub from '@graphile/pg-pubsub';
|
|
9
|
+
|
|
10
|
+
export interface PostGraphileConfigOptions {
|
|
11
|
+
port: string | number;
|
|
12
|
+
nodeEnv: string;
|
|
13
|
+
graphqlEndpoint: string;
|
|
14
|
+
enableSubscriptions: string;
|
|
15
|
+
enableCors: string;
|
|
16
|
+
databaseUrl: string;
|
|
17
|
+
availableTables: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 创建 PostGraphile 配置
|
|
21
|
+
export function createPostGraphileConfig(options: PostGraphileConfigOptions) {
|
|
22
|
+
const { port, nodeEnv, graphqlEndpoint, enableSubscriptions, enableCors, availableTables } =
|
|
23
|
+
options;
|
|
24
|
+
|
|
25
|
+
// 构建GraphQL和WebSocket端点URL
|
|
26
|
+
const baseUrl = `http://localhost:${port}`;
|
|
27
|
+
const graphqlUrl = `${baseUrl}${graphqlEndpoint}`;
|
|
28
|
+
const subscriptionUrl =
|
|
29
|
+
enableSubscriptions === 'true' ? `ws://localhost:${port}${graphqlEndpoint}` : undefined;
|
|
30
|
+
|
|
31
|
+
// 创建插件钩子以支持WebSocket和订阅
|
|
32
|
+
const pluginHook = makePluginHook([PgPubSub]);
|
|
33
|
+
|
|
34
|
+
const config = {
|
|
35
|
+
// 基础配置 - 关闭默认GraphiQL
|
|
36
|
+
graphiql: false,
|
|
37
|
+
enhanceGraphiql: false,
|
|
38
|
+
showErrorStack: nodeEnv === 'development',
|
|
39
|
+
extendedErrors: nodeEnv === 'development' ? ['hint', 'detail', 'errcode'] : [],
|
|
40
|
+
|
|
41
|
+
// 功能配置 - 启用订阅
|
|
42
|
+
subscriptions: enableSubscriptions === 'true',
|
|
43
|
+
live: enableSubscriptions === 'true', // 启用live功能以支持订阅
|
|
44
|
+
enableQueryBatching: true,
|
|
45
|
+
enableCors: enableCors === 'true',
|
|
46
|
+
|
|
47
|
+
// 添加插件钩子以支持WebSocket
|
|
48
|
+
pluginHook,
|
|
49
|
+
|
|
50
|
+
// 禁用所有mutation功能 - 只保留查询和订阅
|
|
51
|
+
disableDefaultMutations: true,
|
|
52
|
+
|
|
53
|
+
// Schema 配置
|
|
54
|
+
dynamicJson: true,
|
|
55
|
+
setofFunctionsContainNulls: false,
|
|
56
|
+
ignoreRBAC: false,
|
|
57
|
+
ignoreIndexes: true,
|
|
58
|
+
|
|
59
|
+
// 日志控制配置
|
|
60
|
+
// 通过环境变量控制SQL查询日志: DISABLE_QUERY_LOG=true 禁用查询日志
|
|
61
|
+
disableQueryLog:
|
|
62
|
+
process.env.DISABLE_QUERY_LOG === 'true' ||
|
|
63
|
+
(nodeEnv === 'production' && process.env.ENABLE_QUERY_LOG !== 'true'),
|
|
64
|
+
|
|
65
|
+
// 启用查询执行计划解释(仅开发环境)
|
|
66
|
+
allowExplain: nodeEnv === 'development',
|
|
67
|
+
|
|
68
|
+
// 监控PostgreSQL变化(仅开发环境)
|
|
69
|
+
watchPg: nodeEnv === 'development',
|
|
70
|
+
|
|
71
|
+
// GraphQL查询超时设置
|
|
72
|
+
queryTimeout: parseInt(process.env.QUERY_TIMEOUT || '30000'),
|
|
73
|
+
|
|
74
|
+
// GraphQL 端点 - 明确指定路由
|
|
75
|
+
graphqlRoute: graphqlEndpoint,
|
|
76
|
+
graphiqlRoute: '/graphiql', // GraphiQL界面路由
|
|
77
|
+
|
|
78
|
+
// 添加自定义插件
|
|
79
|
+
appendPlugins: [
|
|
80
|
+
QueryFilterPlugin, // 必须在SimpleNamingPlugin之前执行
|
|
81
|
+
PgSimplifyInflectorPlugin, // 简化字段名,去掉ByXxxAndYyy后缀
|
|
82
|
+
SimpleNamingPlugin, // 已修复字段丢失问题
|
|
83
|
+
ConnectionFilterPlugin,
|
|
84
|
+
AllFieldsFilterPlugin
|
|
85
|
+
],
|
|
86
|
+
|
|
87
|
+
// Connection Filter 插件的高级配置选项
|
|
88
|
+
graphileBuildOptions: {
|
|
89
|
+
// 启用所有支持的操作符
|
|
90
|
+
connectionFilterAllowedOperators: [
|
|
91
|
+
'isNull',
|
|
92
|
+
'equalTo',
|
|
93
|
+
'notEqualTo',
|
|
94
|
+
'distinctFrom',
|
|
95
|
+
'notDistinctFrom',
|
|
96
|
+
'lessThan',
|
|
97
|
+
'lessThanOrEqualTo',
|
|
98
|
+
'greaterThan',
|
|
99
|
+
'greaterThanOrEqualTo',
|
|
100
|
+
'in',
|
|
101
|
+
'notIn',
|
|
102
|
+
'like',
|
|
103
|
+
'notLike',
|
|
104
|
+
'ilike',
|
|
105
|
+
'notIlike',
|
|
106
|
+
'similarTo',
|
|
107
|
+
'notSimilarTo',
|
|
108
|
+
'includes',
|
|
109
|
+
'notIncludes',
|
|
110
|
+
'includesInsensitive',
|
|
111
|
+
'notIncludesInsensitive',
|
|
112
|
+
'startsWith',
|
|
113
|
+
'notStartsWith',
|
|
114
|
+
'startsWithInsensitive',
|
|
115
|
+
'notStartsWithInsensitive',
|
|
116
|
+
'endsWith',
|
|
117
|
+
'notEndsWith',
|
|
118
|
+
'endsWithInsensitive',
|
|
119
|
+
'notEndsWithInsensitive'
|
|
120
|
+
],
|
|
121
|
+
|
|
122
|
+
// 支持所有字段类型的过滤 - 明确允许所有类型
|
|
123
|
+
connectionFilterAllowedFieldTypes: [
|
|
124
|
+
'String',
|
|
125
|
+
'Int',
|
|
126
|
+
'Float',
|
|
127
|
+
'Boolean',
|
|
128
|
+
'ID',
|
|
129
|
+
'Date',
|
|
130
|
+
'Time',
|
|
131
|
+
'Datetime',
|
|
132
|
+
'JSON',
|
|
133
|
+
'BigInt'
|
|
134
|
+
],
|
|
135
|
+
|
|
136
|
+
// 启用逻辑操作符 (and, or, not)
|
|
137
|
+
connectionFilterLogicalOperators: true,
|
|
138
|
+
|
|
139
|
+
// 启用关系过滤
|
|
140
|
+
connectionFilterRelations: true,
|
|
141
|
+
|
|
142
|
+
// 启用计算列过滤
|
|
143
|
+
connectionFilterComputedColumns: true,
|
|
144
|
+
|
|
145
|
+
// 启用数组过滤
|
|
146
|
+
connectionFilterArrays: true,
|
|
147
|
+
|
|
148
|
+
// 启用函数过滤
|
|
149
|
+
connectionFilterSetofFunctions: true,
|
|
150
|
+
|
|
151
|
+
// 允许空输入和空对象输入
|
|
152
|
+
connectionFilterAllowNullInput: true,
|
|
153
|
+
connectionFilterAllowEmptyObjectInput: true
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
// 只包含检测到的表
|
|
157
|
+
includeExtensionResources: false,
|
|
158
|
+
|
|
159
|
+
// 排除不需要的表
|
|
160
|
+
ignoreTable: (tableName: string) => {
|
|
161
|
+
// 如果没有检测到任何表,允许所有表
|
|
162
|
+
if (availableTables.length === 0) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
// 否则只包含检测到的表
|
|
166
|
+
return !availableTables.includes(tableName);
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
// 导出 schema(开发环境)
|
|
170
|
+
exportGqlSchemaPath: nodeEnv === 'development' ? 'sui-indexer-schema.graphql' : undefined
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// 如果启用订阅,添加额外的PostgreSQL订阅配置
|
|
174
|
+
if (enableSubscriptions === 'true') {
|
|
175
|
+
return {
|
|
176
|
+
...config,
|
|
177
|
+
// 使用专用数据库连接用于订阅
|
|
178
|
+
ownerConnectionString: options.databaseUrl,
|
|
179
|
+
|
|
180
|
+
// WebSocket配置
|
|
181
|
+
websocketMiddlewares: [],
|
|
182
|
+
|
|
183
|
+
// PostgreSQL设置 - 为订阅优化
|
|
184
|
+
pgSettings: {
|
|
185
|
+
statement_timeout: '30s',
|
|
186
|
+
// 为订阅设置适当的事务隔离级别
|
|
187
|
+
default_transaction_isolation: 'read committed'
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
// 连接失败时重试
|
|
191
|
+
retryOnInitFail: true,
|
|
192
|
+
|
|
193
|
+
// 性能优化
|
|
194
|
+
pgDefaultRole: undefined,
|
|
195
|
+
jwtSecret: undefined,
|
|
196
|
+
|
|
197
|
+
// 开发环境的额外配置
|
|
198
|
+
...(nodeEnv === 'development' && {
|
|
199
|
+
queryCache: true,
|
|
200
|
+
allowExplain: true
|
|
201
|
+
})
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return config;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 导出增强版playground的HTML生成器
|
|
209
|
+
export function createPlaygroundHtml(options: PostGraphileConfigOptions): string {
|
|
210
|
+
const { port, graphqlEndpoint, enableSubscriptions, availableTables } = options;
|
|
211
|
+
|
|
212
|
+
// 构建GraphQL和WebSocket端点URL
|
|
213
|
+
const baseUrl = `http://localhost:${port}`;
|
|
214
|
+
const graphqlUrl = `${baseUrl}${graphqlEndpoint}`;
|
|
215
|
+
const subscriptionUrl =
|
|
216
|
+
enableSubscriptions === 'true' ? `ws://localhost:${port}${graphqlEndpoint}` : undefined;
|
|
217
|
+
|
|
218
|
+
return createEnhancedPlayground({
|
|
219
|
+
url: graphqlUrl,
|
|
220
|
+
subscriptionUrl,
|
|
221
|
+
title: 'Sui Indexer GraphQL Playground',
|
|
222
|
+
subtitle: `强大的GraphQL API | 已发现 ${availableTables.length} 个表 | ${
|
|
223
|
+
enableSubscriptions === 'true' ? '支持实时订阅' : '实时订阅已禁用'
|
|
224
|
+
}`
|
|
225
|
+
})(null as any, null as any, {});
|
|
226
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// PostGraphile plugin for filtering queries
|
|
2
|
+
import { Plugin } from 'postgraphile';
|
|
3
|
+
|
|
4
|
+
// Query filter plugin - only keep useful table-related queries
|
|
5
|
+
export const QueryFilterPlugin: Plugin = (builder) => {
|
|
6
|
+
// Filter query fields
|
|
7
|
+
builder.hook('GraphQLObjectType:fields', (fields, build, context) => {
|
|
8
|
+
const {
|
|
9
|
+
scope: { isRootQuery }
|
|
10
|
+
} = context;
|
|
11
|
+
|
|
12
|
+
if (!isRootQuery) {
|
|
13
|
+
return fields;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Define query types to keep
|
|
17
|
+
const allowedQueries = new Set<string>();
|
|
18
|
+
|
|
19
|
+
// Get all table-related queries
|
|
20
|
+
Object.keys(fields).forEach((fieldName) => {
|
|
21
|
+
// Keep PostGraphile required system fields
|
|
22
|
+
if (['query', 'nodeId', 'node'].includes(fieldName)) {
|
|
23
|
+
allowedQueries.add(fieldName);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Keep store table-related queries
|
|
27
|
+
if (fieldName.match(/^(allStore|store)/i)) {
|
|
28
|
+
allowedQueries.add(fieldName);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Keep table_fields table queries
|
|
32
|
+
if (fieldName.match(/^(allTable|table)/i)) {
|
|
33
|
+
allowedQueries.add(fieldName);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Filter fields, only keep allowed queries
|
|
38
|
+
const filteredFields: typeof fields = {};
|
|
39
|
+
Object.keys(fields).forEach((fieldName) => {
|
|
40
|
+
if (allowedQueries.has(fieldName)) {
|
|
41
|
+
filteredFields[fieldName] = fields[fieldName];
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// console.log('🔍 Filtered query fields:', Object.keys(filteredFields));
|
|
46
|
+
return filteredFields;
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export default QueryFilterPlugin;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Plugin } from 'postgraphile';
|
|
2
|
+
|
|
3
|
+
export const SimpleNamingPlugin: Plugin = (builder) => {
|
|
4
|
+
// Rename query fields
|
|
5
|
+
builder.hook('GraphQLObjectType:fields', (fields, build, context) => {
|
|
6
|
+
const {
|
|
7
|
+
scope: { isRootQuery }
|
|
8
|
+
} = context;
|
|
9
|
+
|
|
10
|
+
if (!isRootQuery) {
|
|
11
|
+
return fields;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Create renamed field mapping
|
|
15
|
+
const renamedFields: typeof fields = {};
|
|
16
|
+
const originalFieldNames = Object.keys(fields);
|
|
17
|
+
|
|
18
|
+
console.log('🔍 Original field list:', originalFieldNames);
|
|
19
|
+
|
|
20
|
+
// For tracking rename mapping
|
|
21
|
+
const renameMap: Record<string, string> = {};
|
|
22
|
+
|
|
23
|
+
originalFieldNames.forEach((fieldName) => {
|
|
24
|
+
let newFieldName = fieldName;
|
|
25
|
+
|
|
26
|
+
// Remove "all" prefix, but keep system fields
|
|
27
|
+
if (
|
|
28
|
+
fieldName.startsWith('all') &&
|
|
29
|
+
!['allRows', 'allTableFields'].includes(fieldName) // Extend reserved list
|
|
30
|
+
) {
|
|
31
|
+
// allStoreAccounts -> storeAccounts
|
|
32
|
+
// allStoreEncounters -> storeEncounters
|
|
33
|
+
newFieldName = fieldName.replace(/^all/, '');
|
|
34
|
+
// First letter to lowercase, maintain camelCase
|
|
35
|
+
if (newFieldName.length > 0) {
|
|
36
|
+
newFieldName = newFieldName.charAt(0).toLowerCase() + newFieldName.slice(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Remove "store" prefix (note lowercase s, because it's already processed above)
|
|
41
|
+
if (newFieldName.startsWith('store') && newFieldName !== 'store') {
|
|
42
|
+
// storeAccounts -> accounts
|
|
43
|
+
// storeAccount -> account
|
|
44
|
+
// storeEncounters -> encounters
|
|
45
|
+
// storeEncounter -> encounter
|
|
46
|
+
const withoutStore = newFieldName.replace(/^store/, '');
|
|
47
|
+
// First letter to lowercase, maintain camelCase
|
|
48
|
+
if (withoutStore.length > 0) {
|
|
49
|
+
const finalName = withoutStore.charAt(0).toLowerCase() + withoutStore.slice(1);
|
|
50
|
+
|
|
51
|
+
// Check if field name conflict will occur
|
|
52
|
+
if (!renamedFields[finalName] && !originalFieldNames.includes(finalName)) {
|
|
53
|
+
newFieldName = finalName;
|
|
54
|
+
}
|
|
55
|
+
// If conflict, keep original name (remove all but keep store)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check if final field name will conflict
|
|
60
|
+
if (renamedFields[newFieldName]) {
|
|
61
|
+
console.warn(`⚠️ Field name conflict: ${newFieldName}, keeping original name ${fieldName}`);
|
|
62
|
+
newFieldName = fieldName; // Keep original name to avoid conflict
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
renameMap[fieldName] = newFieldName;
|
|
66
|
+
renamedFields[newFieldName] = fields[fieldName];
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const renamedCount = Object.entries(renameMap).filter(
|
|
70
|
+
([old, newName]) => old !== newName
|
|
71
|
+
).length;
|
|
72
|
+
const finalFieldNames = Object.keys(renamedFields);
|
|
73
|
+
|
|
74
|
+
console.log('🔄 Field rename statistics:', {
|
|
75
|
+
'Original field count': originalFieldNames.length,
|
|
76
|
+
'Final field count': finalFieldNames.length,
|
|
77
|
+
'Renamed field count': renamedCount
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (renamedCount > 0) {
|
|
81
|
+
console.log(
|
|
82
|
+
'📝 Rename mapping:',
|
|
83
|
+
Object.entries(renameMap)
|
|
84
|
+
.filter(([old, newName]) => old !== newName)
|
|
85
|
+
.reduce((acc, [old, newName]) => ({ ...acc, [old]: newName }), {})
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Ensure field count is not lost
|
|
90
|
+
if (finalFieldNames.length !== originalFieldNames.length) {
|
|
91
|
+
console.error(
|
|
92
|
+
'❌ Fields lost! Original:',
|
|
93
|
+
originalFieldNames.length,
|
|
94
|
+
'Final:',
|
|
95
|
+
finalFieldNames.length
|
|
96
|
+
);
|
|
97
|
+
// If fields are lost, return original fields to avoid breakage
|
|
98
|
+
return fields;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return renamedFields;
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export default SimpleNamingPlugin;
|