@0xobelisk/graphql-server 1.2.0-pre.100
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/.turbo/turbo-build.log +8 -0
- package/DUAL_POOL_CONFIG.md +188 -0
- package/Dockerfile +35 -0
- package/LICENSE +92 -0
- package/README.md +487 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +206 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/subscription-config.d.ts +80 -0
- package/dist/config/subscription-config.d.ts.map +1 -0
- package/dist/config/subscription-config.js +158 -0
- package/dist/config/subscription-config.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -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 +113 -0
- package/dist/plugins/enhanced-playground.js.map +1 -0
- package/dist/plugins/enhanced-server-manager.d.ts +29 -0
- package/dist/plugins/enhanced-server-manager.d.ts.map +1 -0
- package/dist/plugins/enhanced-server-manager.js +262 -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 +138 -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/server.d.ts +21 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +265 -0
- package/dist/server.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 +230 -0
- package/dist/utils/logger/index.js.map +1 -0
- package/docker-compose.yml +46 -0
- package/eslint.config.mjs +3 -0
- package/package.json +78 -0
- package/src/cli.ts +232 -0
- package/src/config/subscription-config.ts +243 -0
- package/src/index.ts +11 -0
- package/src/plugins/README.md +138 -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 +121 -0
- package/src/plugins/enhanced-server-manager.ts +314 -0
- package/src/plugins/index.ts +9 -0
- package/src/plugins/postgraphile-config.ts +182 -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/server.ts +324 -0
- package/src/universal-subscriptions.ts +397 -0
- package/src/utils/logger/README.md +209 -0
- package/src/utils/logger/index.ts +275 -0
- package/sui-indexer-schema.graphql +3691 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,314 @@
|
|
|
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
|
+
private pgPool: Pool | null = null;
|
|
29
|
+
|
|
30
|
+
constructor() {
|
|
31
|
+
this.config = subscriptionConfig.getConfig();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Create Express application
|
|
35
|
+
private createExpressApp(serverConfig: EnhancedServerConfig): Express {
|
|
36
|
+
const { postgraphileMiddleware, allTables, welcomeConfig, postgraphileConfigOptions } =
|
|
37
|
+
serverConfig;
|
|
38
|
+
|
|
39
|
+
const app = express();
|
|
40
|
+
|
|
41
|
+
// Middleware configuration
|
|
42
|
+
app.use(
|
|
43
|
+
cors({
|
|
44
|
+
origin: '*',
|
|
45
|
+
methods: ['GET', 'POST', 'OPTIONS'],
|
|
46
|
+
allowedHeaders: ['Content-Type', 'Authorization']
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Request logging middleware
|
|
51
|
+
app.use((req: Request, res: Response, next) => {
|
|
52
|
+
const startTime = Date.now();
|
|
53
|
+
|
|
54
|
+
res.on('finish', () => {
|
|
55
|
+
logExpress(req.method, req.path, res.statusCode, startTime, {
|
|
56
|
+
userAgent: req.get('user-agent')?.substring(0, 50)
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
next();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Route configuration
|
|
64
|
+
|
|
65
|
+
// Root path - welcome page
|
|
66
|
+
app.get('/', (req: Request, res: Response) => {
|
|
67
|
+
res.set('Content-Type', 'text/html; charset=utf-8');
|
|
68
|
+
res.send(createWelcomePage(allTables, welcomeConfig));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// GraphQL Playground
|
|
72
|
+
app.get('/playground', (req: Request, res: Response) => {
|
|
73
|
+
res.set('Content-Type', 'text/html; charset=utf-8');
|
|
74
|
+
res.send(createPlaygroundHtml(postgraphileConfigOptions));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Redirect old GraphiQL paths
|
|
78
|
+
app.get('/graphiql*', (req: Request, res: Response) => {
|
|
79
|
+
serverLogger.info('Redirecting old GraphiQL path', {
|
|
80
|
+
from: req.path,
|
|
81
|
+
to: '/playground'
|
|
82
|
+
});
|
|
83
|
+
res.redirect(301, '/playground');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Health check endpoint
|
|
87
|
+
app.get('/health', (req: Request, res: Response) => {
|
|
88
|
+
res.json({
|
|
89
|
+
status: 'healthy',
|
|
90
|
+
subscriptions: this.getSubscriptionStatus(),
|
|
91
|
+
timestamp: new Date().toISOString()
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Subscription configuration endpoint
|
|
96
|
+
app.get('/subscription-config', (req: Request, res: Response) => {
|
|
97
|
+
res.json(subscriptionConfig.generateClientConfig());
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Configuration documentation endpoint
|
|
101
|
+
app.get('/subscription-docs', (req: Request, res: Response) => {
|
|
102
|
+
res.set('Content-Type', 'text/plain');
|
|
103
|
+
res.send(subscriptionConfig.generateDocumentation());
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Add connection pool status endpoint
|
|
107
|
+
app.get('/pool-status', (req, res) => {
|
|
108
|
+
if (this.pgPool) {
|
|
109
|
+
const poolStatus = {
|
|
110
|
+
totalCount: this.pgPool.totalCount,
|
|
111
|
+
idleCount: this.pgPool.idleCount,
|
|
112
|
+
waitingCount: this.pgPool.waitingCount,
|
|
113
|
+
maxConnections: this.pgPool.options.max || 'Not set',
|
|
114
|
+
minConnections: this.pgPool.options.min || 'Not set'
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
res.json({
|
|
118
|
+
status: 'ok',
|
|
119
|
+
connectionPool: poolStatus,
|
|
120
|
+
strategy: 'single-pool-unified',
|
|
121
|
+
operations: ['query', 'mutation', 'subscription'],
|
|
122
|
+
timestamp: new Date().toISOString(),
|
|
123
|
+
uptime: process.uptime(),
|
|
124
|
+
memory: process.memoryUsage()
|
|
125
|
+
});
|
|
126
|
+
} else {
|
|
127
|
+
res.status(503).json({
|
|
128
|
+
status: 'error',
|
|
129
|
+
message: 'Connection pool not available'
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// PostGraphile middleware - mount at root path, let PostGraphile handle routing itself
|
|
135
|
+
app.use((req: Request, res: Response, next) => {
|
|
136
|
+
// Check if PostGraphile middleware exists
|
|
137
|
+
if (!postgraphileMiddleware) {
|
|
138
|
+
console.error('❌ PostGraphile middleware is null!');
|
|
139
|
+
if (req.path.startsWith('/graphql')) {
|
|
140
|
+
res.status(500).json({
|
|
141
|
+
error: 'PostGraphile middleware not properly initialized'
|
|
142
|
+
});
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
next();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
postgraphileMiddleware(req, res, next);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error('❌ PostGraphile middleware execution error:', error);
|
|
153
|
+
if (req.path.startsWith('/graphql')) {
|
|
154
|
+
res.status(500).json({
|
|
155
|
+
error: 'PostGraphile execution error',
|
|
156
|
+
details: error instanceof Error ? error.message : String(error)
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
next();
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Error handling middleware
|
|
165
|
+
app.use((err: Error, req: Request, res: Response, _next: express.NextFunction) => {
|
|
166
|
+
serverLogger.error('Express error handling', err, {
|
|
167
|
+
url: req.originalUrl,
|
|
168
|
+
method: req.method,
|
|
169
|
+
userAgent: req.get('user-agent')?.substring(0, 50)
|
|
170
|
+
});
|
|
171
|
+
res.status(500).send('Internal Server Error');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return app;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Create and configure HTTP server
|
|
178
|
+
async createEnhancedServer(serverConfig: EnhancedServerConfig): Promise<HttpServer> {
|
|
179
|
+
const { postgraphileMiddleware, pgPool } = serverConfig;
|
|
180
|
+
|
|
181
|
+
// Store pool references for monitoring
|
|
182
|
+
this.pgPool = pgPool;
|
|
183
|
+
|
|
184
|
+
// Create Express application
|
|
185
|
+
this.app = this.createExpressApp(serverConfig);
|
|
186
|
+
|
|
187
|
+
// Create HTTP server
|
|
188
|
+
this.httpServer = createServer(this.app);
|
|
189
|
+
|
|
190
|
+
// Enable PostgreSQL subscriptions and WebSocket support
|
|
191
|
+
if (this.config.capabilities.pgSubscriptions) {
|
|
192
|
+
enhanceHttpServerWithSubscriptions(this.httpServer, postgraphileMiddleware, {
|
|
193
|
+
// Enable WebSocket transport
|
|
194
|
+
graphqlRoute: '/graphql'
|
|
195
|
+
});
|
|
196
|
+
systemLogger.info('✅ PostgreSQL subscriptions and WebSocket enabled', {
|
|
197
|
+
pgSubscriptions: this.config.capabilities.pgSubscriptions,
|
|
198
|
+
webSocket: true
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
serverLogger.info('🚀 Express server creation completed', {
|
|
203
|
+
framework: 'Express',
|
|
204
|
+
graphqlPort: this.config.graphqlPort,
|
|
205
|
+
capabilities: {
|
|
206
|
+
pgSubscriptions: this.config.capabilities.pgSubscriptions
|
|
207
|
+
},
|
|
208
|
+
recommendedMethod: 'pg-subscriptions'
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return this.httpServer;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Start server
|
|
215
|
+
async startServer(): Promise<void> {
|
|
216
|
+
if (!this.httpServer) {
|
|
217
|
+
throw new Error('Server not created, please call createEnhancedServer() first');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return new Promise((resolve, reject) => {
|
|
221
|
+
this.httpServer!.listen(this.config.graphqlPort, (err?: Error) => {
|
|
222
|
+
if (err) {
|
|
223
|
+
reject(err);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
this.logServerStatus();
|
|
228
|
+
resolve();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Log server status
|
|
234
|
+
private logServerStatus() {
|
|
235
|
+
const clientConfig = subscriptionConfig.generateClientConfig();
|
|
236
|
+
|
|
237
|
+
serverLogger.info('🎉 Express GraphQL server started successfully!', {
|
|
238
|
+
port: this.config.graphqlPort,
|
|
239
|
+
framework: 'Express',
|
|
240
|
+
endpoints: {
|
|
241
|
+
home: `http://localhost:${this.config.graphqlPort}/`,
|
|
242
|
+
playground: `http://localhost:${this.config.graphqlPort}/playground`,
|
|
243
|
+
graphql: clientConfig.graphqlEndpoint,
|
|
244
|
+
subscription: clientConfig.subscriptionEndpoint,
|
|
245
|
+
health: `http://localhost:${this.config.graphqlPort}/health`,
|
|
246
|
+
config: `http://localhost:${this.config.graphqlPort}/subscription-config`,
|
|
247
|
+
docs: `http://localhost:${this.config.graphqlPort}/subscription-docs`
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Display main access links
|
|
252
|
+
console.log('\n' + '🌟'.repeat(30));
|
|
253
|
+
console.log('🏠 Homepage: ' + `http://localhost:${this.config.graphqlPort}/`);
|
|
254
|
+
console.log('🎮 Playground: ' + `http://localhost:${this.config.graphqlPort}/playground`);
|
|
255
|
+
console.log('🔗 GraphQL: ' + clientConfig.graphqlEndpoint);
|
|
256
|
+
console.log('📡 WebSocket: ' + clientConfig.subscriptionEndpoint);
|
|
257
|
+
console.log('🌟'.repeat(30) + '\n');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Get subscription status
|
|
261
|
+
private getSubscriptionStatus() {
|
|
262
|
+
return {
|
|
263
|
+
enabled: this.config.capabilities.pgSubscriptions,
|
|
264
|
+
method: 'pg-subscriptions',
|
|
265
|
+
config: subscriptionConfig.generateClientConfig()
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Quick shutdown
|
|
270
|
+
async quickShutdown(): Promise<void> {
|
|
271
|
+
systemLogger.info('🛑 Starting quick shutdown of Express server...');
|
|
272
|
+
|
|
273
|
+
if (this.httpServer) {
|
|
274
|
+
this.httpServer.close();
|
|
275
|
+
systemLogger.info('✅ HTTP server closed');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
systemLogger.info('🎯 Express server quick shutdown completed');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Graceful shutdown
|
|
282
|
+
async gracefulShutdown(pgPool: Pool): Promise<void> {
|
|
283
|
+
systemLogger.info('🛑 Starting graceful shutdown of Express server...');
|
|
284
|
+
|
|
285
|
+
const shutdownPromises: Promise<void>[] = [];
|
|
286
|
+
|
|
287
|
+
// Close HTTP server
|
|
288
|
+
if (this.httpServer) {
|
|
289
|
+
shutdownPromises.push(
|
|
290
|
+
new Promise((resolve) => {
|
|
291
|
+
this.httpServer!.close(() => {
|
|
292
|
+
systemLogger.info('✅ HTTP server closed');
|
|
293
|
+
resolve();
|
|
294
|
+
});
|
|
295
|
+
})
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Close database connection pool
|
|
300
|
+
shutdownPromises.push(
|
|
301
|
+
pgPool.end().then(() => {
|
|
302
|
+
systemLogger.info('✅ Database connection pool closed');
|
|
303
|
+
})
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
await Promise.all(shutdownPromises);
|
|
308
|
+
systemLogger.info('🎯 Express server graceful shutdown completed');
|
|
309
|
+
} catch (error) {
|
|
310
|
+
systemLogger.error('❌ Error occurred during shutdown process', error);
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Unified export entry point for plugins
|
|
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,182 @@
|
|
|
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
|
+
// Additional configuration from CLI
|
|
19
|
+
disableQueryLog: boolean;
|
|
20
|
+
enableQueryLog: boolean;
|
|
21
|
+
queryTimeout: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Create PostGraphile configuration
|
|
25
|
+
export function createPostGraphileConfig(options: PostGraphileConfigOptions) {
|
|
26
|
+
const { port, nodeEnv, graphqlEndpoint, enableSubscriptions, enableCors, availableTables } =
|
|
27
|
+
options;
|
|
28
|
+
|
|
29
|
+
// Build GraphQL and WebSocket endpoint URLs
|
|
30
|
+
const baseUrl = `http://localhost:${port}`;
|
|
31
|
+
const _graphqlUrl = `${baseUrl}${graphqlEndpoint}`;
|
|
32
|
+
const _subscriptionUrl =
|
|
33
|
+
enableSubscriptions === 'true' ? `ws://localhost:${port}${graphqlEndpoint}` : undefined;
|
|
34
|
+
|
|
35
|
+
// Create plugin hook to support WebSocket and subscriptions
|
|
36
|
+
const pluginHook = makePluginHook([PgPubSub]);
|
|
37
|
+
|
|
38
|
+
const config = {
|
|
39
|
+
// Basic configuration - disable default GraphiQL
|
|
40
|
+
graphiql: false,
|
|
41
|
+
enhanceGraphiql: false,
|
|
42
|
+
showErrorStack: nodeEnv === 'development',
|
|
43
|
+
extendedErrors: nodeEnv === 'development' ? ['hint', 'detail', 'errcode'] : [],
|
|
44
|
+
|
|
45
|
+
// Feature configuration - enable subscriptions
|
|
46
|
+
subscriptions: enableSubscriptions === 'true',
|
|
47
|
+
live: enableSubscriptions === 'true', // Enable live functionality to support subscriptions
|
|
48
|
+
enableQueryBatching: true,
|
|
49
|
+
enableCors: enableCors === 'true',
|
|
50
|
+
|
|
51
|
+
// Add plugin hook to support WebSocket
|
|
52
|
+
pluginHook,
|
|
53
|
+
|
|
54
|
+
// Disable all mutation functionality - only keep queries and subscriptions
|
|
55
|
+
disableDefaultMutations: true,
|
|
56
|
+
|
|
57
|
+
// Schema configuration
|
|
58
|
+
dynamicJson: true,
|
|
59
|
+
setofFunctionsContainNulls: false,
|
|
60
|
+
ignoreRBAC: false,
|
|
61
|
+
ignoreIndexes: true,
|
|
62
|
+
|
|
63
|
+
// Log control configuration
|
|
64
|
+
// Control SQL query logs through CLI parameters
|
|
65
|
+
disableQueryLog:
|
|
66
|
+
options.disableQueryLog || (nodeEnv === 'production' && !options.enableQueryLog),
|
|
67
|
+
|
|
68
|
+
// Enable query execution plan explanation (development environment only)
|
|
69
|
+
allowExplain: nodeEnv === 'development',
|
|
70
|
+
|
|
71
|
+
// Monitor PostgreSQL changes (development environment only)
|
|
72
|
+
watchPg: nodeEnv === 'development',
|
|
73
|
+
|
|
74
|
+
// GraphQL query timeout setting
|
|
75
|
+
queryTimeout: options.queryTimeout,
|
|
76
|
+
|
|
77
|
+
// GraphQL endpoint - explicitly specify route
|
|
78
|
+
graphqlRoute: graphqlEndpoint,
|
|
79
|
+
graphiqlRoute: '/graphiql', // GraphiQL interface route
|
|
80
|
+
|
|
81
|
+
// Add custom plugins
|
|
82
|
+
appendPlugins: [
|
|
83
|
+
QueryFilterPlugin, // Must execute before SimpleNamingPlugin
|
|
84
|
+
PgSimplifyInflectorPlugin, // Simplify field names, remove ByXxxAndYyy suffixes
|
|
85
|
+
SimpleNamingPlugin, // Fixed field loss issue
|
|
86
|
+
ConnectionFilterPlugin,
|
|
87
|
+
AllFieldsFilterPlugin
|
|
88
|
+
],
|
|
89
|
+
|
|
90
|
+
// Advanced configuration options for Connection Filter plugin
|
|
91
|
+
graphileBuildOptions: {
|
|
92
|
+
// Enable logical operators (and, or, not)
|
|
93
|
+
connectionFilterLogicalOperators: true,
|
|
94
|
+
|
|
95
|
+
// Enable relationship filtering
|
|
96
|
+
connectionFilterRelations: true,
|
|
97
|
+
|
|
98
|
+
// Enable computed column filtering
|
|
99
|
+
connectionFilterComputedColumns: true,
|
|
100
|
+
|
|
101
|
+
// Enable array filtering
|
|
102
|
+
connectionFilterArrays: true,
|
|
103
|
+
|
|
104
|
+
// Enable function filtering
|
|
105
|
+
connectionFilterSetofFunctions: true,
|
|
106
|
+
|
|
107
|
+
// Allow null input and empty object input
|
|
108
|
+
connectionFilterAllowNullInput: true,
|
|
109
|
+
connectionFilterAllowEmptyObjectInput: true
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
// Only include detected tables
|
|
113
|
+
includeExtensionResources: false,
|
|
114
|
+
|
|
115
|
+
// Exclude unnecessary tables
|
|
116
|
+
ignoreTable: (tableName: string) => {
|
|
117
|
+
// If no tables detected, allow all tables
|
|
118
|
+
if (availableTables.length === 0) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
// Otherwise only include detected tables
|
|
122
|
+
return !availableTables.includes(tableName);
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
// Export schema (development environment)
|
|
126
|
+
exportGqlSchemaPath: nodeEnv === 'development' ? 'sui-indexer-schema.graphql' : undefined
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// If subscriptions are enabled, add additional PostgreSQL subscription configuration
|
|
130
|
+
if (enableSubscriptions === 'true') {
|
|
131
|
+
return {
|
|
132
|
+
...config,
|
|
133
|
+
// Use dedicated subscription connection pool
|
|
134
|
+
ownerConnectionString: options.databaseUrl,
|
|
135
|
+
|
|
136
|
+
// WebSocket configuration
|
|
137
|
+
websocketMiddlewares: [],
|
|
138
|
+
|
|
139
|
+
// PostgreSQL settings - optimized for long-running subscriptions
|
|
140
|
+
pgSettings: {
|
|
141
|
+
statement_timeout: '0', // No timeout for subscription queries
|
|
142
|
+
idle_in_transaction_session_timeout: '0', // Allow long transactions
|
|
143
|
+
default_transaction_isolation: 'read committed'
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
// Retry on connection failure
|
|
147
|
+
retryOnInitFail: true,
|
|
148
|
+
|
|
149
|
+
// Performance optimization for subscriptions
|
|
150
|
+
pgDefaultRole: undefined,
|
|
151
|
+
jwtSecret: undefined,
|
|
152
|
+
|
|
153
|
+
// Additional configuration for development environment
|
|
154
|
+
...(nodeEnv === 'development' && {
|
|
155
|
+
queryCache: false, // Disable cache for real-time data
|
|
156
|
+
allowExplain: true
|
|
157
|
+
})
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return config;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Export enhanced playground HTML generator
|
|
165
|
+
export function createPlaygroundHtml(options: PostGraphileConfigOptions): string {
|
|
166
|
+
const { graphqlEndpoint, enableSubscriptions, availableTables } = options;
|
|
167
|
+
|
|
168
|
+
// Use relative URLs so playground connects to the same domain as the server
|
|
169
|
+
const graphqlUrl = graphqlEndpoint;
|
|
170
|
+
const subscriptionUrl = enableSubscriptions === 'true' ? graphqlEndpoint : undefined;
|
|
171
|
+
|
|
172
|
+
return createEnhancedPlayground({
|
|
173
|
+
url: graphqlUrl,
|
|
174
|
+
subscriptionUrl,
|
|
175
|
+
title: 'Sui Indexer GraphQL Playground',
|
|
176
|
+
subtitle: `Powerful GraphQL API | ${availableTables.length} tables discovered | ${
|
|
177
|
+
enableSubscriptions === 'true'
|
|
178
|
+
? 'Real-time subscriptions supported'
|
|
179
|
+
: 'Real-time subscriptions disabled'
|
|
180
|
+
}`
|
|
181
|
+
})(null as any, null as any, {});
|
|
182
|
+
}
|
|
@@ -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;
|