@queryleaf/postgres-server
Version:
PostgreSQL wire-compatible server for QueryLeaf
250 lines (220 loc) • 7.36 kB
text/typescript
import { createServer, Server } from 'net';
import { MongoClient } from 'mongodb';
import { QueryLeaf } from '@queryleaf/lib';
import { ProtocolHandler } from './protocol-handler';
import debugLib from 'debug';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
const debug = debugLib('queryleaf:pg-server');
// Parse command line arguments
// Only parse if this file is the main entry point (not during tests)
const yargsInstance = yargs(hideBin(process.argv))
.usage('Usage: $0 [options]')
.option('uri', {
describe: 'MongoDB connection URI',
default: 'mongodb://localhost:27017',
type: 'string',
})
.option('db', {
describe: 'MongoDB database name',
demandOption: true,
type: 'string',
})
.option('port', {
describe: 'PostgreSQL server port',
default: 5432,
type: 'number',
})
.option('host', {
describe: 'PostgreSQL server host',
default: 'localhost',
type: 'string',
})
.option('max-connections', {
describe: 'Maximum number of connections',
default: 100,
type: 'number',
})
.option('auth-passthrough', {
describe: 'Pass authentication credentials to MongoDB',
default: false,
type: 'boolean',
})
.example('$0 --db mydb --port 5432', 'Start the PostgreSQL-compatible server on port 5432')
.example(
'$0 --db mydb --uri mongodb://username:password@localhost:27017',
'Connect to MongoDB with authentication'
)
.epilog('For more information, visit https://github.com/beekeeper-studio/queryleaf')
.help()
.alias('help', 'h')
.version()
.alias('version', 'v');
// Define the argument type
interface Args {
uri?: string;
db?: string;
port?: number;
host?: string;
maxConnections?: number;
authPassthrough?: boolean;
}
// Only parse command line args when run as main file, not during tests
const argv: Args = require.main === module ? yargsInstance.parseSync() : {};
/**
* PostgreSQL wire protocol server for QueryLeaf
*/
class PostgresServer {
private server: Server;
private mongoClient: MongoClient;
private queryLeaf: QueryLeaf;
private connections: Set<ProtocolHandler> = new Set();
private maxConnections: number;
private authPassthrough: boolean;
private dbName: string;
private mongoUri: string;
/**
* Create a new PostgreSQL wire protocol server
* @param mongoClient MongoDB client
* @param dbName MongoDB database name
* @param options Server options
*/
constructor(
mongoClient: MongoClient,
dbName: string,
options: {
port: number;
host: string;
maxConnections: number;
authPassthrough?: boolean;
mongoUri?: string;
}
) {
this.mongoClient = mongoClient;
this.queryLeaf = new QueryLeaf(mongoClient, dbName);
this.maxConnections = options.maxConnections;
this.authPassthrough = options.authPassthrough || false;
this.dbName = dbName;
this.mongoUri = options.mongoUri || '';
// Create TCP server
this.server = createServer((socket) => {
debug(`New connection from ${socket.remoteAddress}:${socket.remotePort}`);
// Check max connections
if (this.connections.size >= this.maxConnections) {
debug(`Connection limit reached (${this.maxConnections}), rejecting connection`);
socket.end();
return;
}
// Create protocol handler for this connection
const handler = new ProtocolHandler(socket, this.queryLeaf, {
authPassthrough: this.authPassthrough,
mongoClient: this.mongoClient,
dbName: this.dbName,
mongoUri: this.mongoUri,
});
this.connections.add(handler);
// Remove connection on close
socket.on('close', () => {
debug(`Connection closed from ${socket.remoteAddress}:${socket.remotePort}`);
this.connections.delete(handler);
});
});
// Handle server errors
this.server.on('error', (err) => {
console.error('Server error:', err);
});
// Handle process signals (only when running as main module)
if (require.main === module) {
process.on('SIGINT', () => this.shutdown(true));
process.on('SIGTERM', () => this.shutdown(true));
// Only auto-listen if running as main module
this.listen(options.port, options.host);
}
// For tests, we'll call listen() explicitly
}
/**
* Start listening for connections
* @param port Port to listen on
* @param host Host to listen on
* @returns A promise that resolves when the server is listening
*/
async listen(port: number, host: string): Promise<void> {
return new Promise((resolve, reject) => {
this.server.listen(port, host, () => {
debug(`PostgreSQL-compatible server running at ${host}:${port}`);
debug(`Connected to MongoDB at ${this.mongoClient.options.hosts?.join(',')}`);
resolve();
});
// Handle initial connection errors
const errorHandler = (err: Error) => {
this.server.removeListener('error', errorHandler);
reject(err);
};
this.server.once('error', errorHandler);
});
}
/**
* Shutdown the server and close all connections
* @param exitProcess Whether to exit the process after shutdown (default: false)
*/
async shutdown(exitProcess = false): Promise<void> {
debug('Shutting down server...');
// Close server - stop accepting new connections
this.server.close();
// Close MongoDB connection
await this.mongoClient.close();
debug('Server stopped');
// Only exit the process if explicitly requested (for CLI usage)
if (exitProcess) {
process.exit(0);
}
}
}
/**
* Main function to start the server
*/
async function main(): Promise<void> {
// Use the command line arguments or default values
const uri = argv.uri || 'mongodb://localhost:27017';
const db = argv.db;
const port = argv.port || 5432;
const host = argv.host || 'localhost';
const maxConnections = argv.maxConnections || 100;
const authPassthrough = argv.authPassthrough || false;
if (!db) {
console.error('Error: MongoDB database name is required. Use --db option.');
process.exit(1);
}
try {
// Connect to MongoDB
debug(`Connecting to MongoDB: ${uri}`);
const mongoClient = new MongoClient(uri);
await mongoClient.connect();
debug(`Connected to MongoDB, using database: ${db}`);
// Start PostgreSQL server
new PostgresServer(mongoClient, db, {
port,
host,
maxConnections,
authPassthrough,
mongoUri: uri,
});
console.log(`🌱 QueryLeaf PostgreSQL-compatible server started at ${host}:${port}`);
console.log(`Connected to MongoDB database: ${db}`);
console.log(`Connection string: postgresql://<username>@${host}:${port}/${db}`);
console.log(`Connect with: psql -h ${host} -p ${port} -d ${db} -U <any-username>`);
console.log('Press Ctrl+C to stop the server');
} catch (error) {
console.error('Error starting server:', error);
process.exit(1);
}
}
// Run the main function if this file is executed directly
if (require.main === module) {
main().catch((error) => {
console.error(`Unhandled error: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
});
}
export { PostgresServer, ProtocolHandler };