@marculosus/local-pg
Version:
Global CLI tool to run PGlite as a PostgreSQL-compatible server with wire protocol support
250 lines (206 loc) • 8.32 kB
JavaScript
import { PGlite } from '@electric-sql/pglite';
import { PGLiteSocketHandler } from '@electric-sql/pglite-socket';
import { createServer } from 'net';
import chalk from 'chalk';
// Parse command line arguments
function parseArgs() {
const args = process.argv.slice(2);
const config = {
db: 'memory://', // Default to in-memory
port: 5432,
host: '127.0.0.1',
debug: 0,
inspect: false,
dbname: 'mydb', // Default database name
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg.startsWith('--db=')) {
config.db = arg.split('=')[1];
} else if (arg.startsWith('--port=')) {
config.port = parseInt(arg.split('=')[1]);
} else if (arg.startsWith('--host=')) {
config.host = arg.split('=')[1];
} else if (arg.startsWith('--debug=')) {
config.debug = parseInt(arg.split('=')[1]);
} else if (arg.startsWith('--dbname=')) {
config.dbname = arg.split('=')[1];
} else if (arg === '--inspect') {
config.inspect = true;
}
}
return config;
}
async function startServer() {
const config = parseArgs();
console.log(chalk.bold.cyan('🚀 Starting Custom PGlite Socket Handler...'));
console.log(`${chalk.bold.cyan('📊 Configuration:')}
${chalk.cyan('Database:')} ${config.db}
${chalk.cyan('DB Name:')} ${config.dbname}
${chalk.cyan('Host:')} ${config.host}
${chalk.cyan('Port:')} ${config.port}
${chalk.cyan('Debug Level:')} ${config.debug}
${chalk.cyan('Inspect Mode:')} ${config.inspect ? 'Enabled' : 'Disabled'}
`);
try {
// Create PGlite instance
console.log(chalk.cyan('🔧 Initializing PGlite database...'));
const db = await PGlite.create({
dataDir: config.db,
debug: config.debug,
relaxedDurability: config.db.startsWith('memory://') ? true : false
});
// Wait for database to be ready
await db.waitReady;
console.log(chalk.green('✅ PGlite database ready'));
// Initialize sample data
await initializeDatabase(db);
// Create a handler
const handler = new PGLiteSocketHandler({
db,
dbname: config.dbname, // Custom database name
closeOnDetach: true, // Automatically close socket when detached
inspect: config.inspect, // Print protocol data for debugging
});
// Create a server that uses the handler
const server = createServer(async (socket) => {
try {
console.log(chalk.yellow(`🔌 Client connecting from ${socket.remoteAddress}:${socket.remotePort}`));
await handler.attach(socket);
console.log(chalk.green(`✅ Client connected from ${socket.remoteAddress}:${socket.remotePort}`));
// Listen for socket events
socket.on('close', () => {
console.log(chalk.yellow(`📴 Client disconnected from ${socket.remoteAddress}:${socket.remotePort}`));
});
socket.on('error', (err) => {
console.error(chalk.red(`❌ Socket error: ${err.message}`));
});
} catch (err) {
console.error(chalk.red('❌ Error attaching socket:'), err);
socket.end();
}
});
// Start listening
server.listen(config.port, config.host, () => {
const address = server.address();
console.log(`
${chalk.bold.green('🎉 PGlite Custom Socket Server is running!')}
${chalk.bold.cyan('Connection Details:')}
${chalk.cyan('Host:')} ${address.address}
${chalk.cyan('Port:')} ${address.port}
${chalk.cyan('Database:')} ${config.dbname}
${chalk.cyan('User:')} postgres ${chalk.gray('(no password required)')}
${chalk.bold.cyan('Connection String:')}
${chalk.yellow(`postgres://postgres@${address.address === '::' || address.address === '0.0.0.0' ? 'localhost' : address.address}:${address.port}/${config.dbname}`)}
${chalk.bold.cyan('Connect using psql:')}
${chalk.yellow(`psql -h ${address.address === '::' || address.address === '0.0.0.0' ? 'localhost' : address.address} -p ${address.port} -U postgres ${config.dbname}`)}
${chalk.bold.yellow('⚠️ Note:')} PGlite supports ${chalk.bold('only ONE connection at a time')}.
If connection fails, ensure no other client is connected.
${chalk.bold.red('🛑 Press Ctrl+C to stop the server')}
`);
});
// Handle server events
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(chalk.red(`❌ Port ${config.port} is already in use. Try a different port.`));
} else {
console.error(chalk.red('❌ Server error:'), err);
}
process.exit(1);
});
// Handle graceful shutdown
const shutdown = async (signal) => {
console.log(`\n${chalk.yellow(`🛑 Received ${signal}, shutting down gracefully...`)}`);
try {
server.close();
handler.detach(true); // Detach and close any connected socket
await db.close();
console.log(chalk.green('✅ Server stopped and database closed'));
process.exit(0);
} catch (err) {
console.error(chalk.red('❌ Error during shutdown:'), err);
process.exit(1);
}
};
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
} catch (error) {
console.error(chalk.red('❌ Failed to start server:'), error);
process.exit(1);
}
}
async function initializeDatabase(db) {
// Create some basic sample data
await db.exec(`
-- Create a sample table for testing
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create a posts table for more complex examples
CREATE TABLE IF NOT EXISTS posts (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
content TEXT,
user_id INTEGER REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Insert sample data if table is empty
INSERT INTO users (name, email)
SELECT 'John Doe', 'john@example.com'
WHERE NOT EXISTS (SELECT 1 FROM users WHERE email = 'john@example.com');
INSERT INTO users (name, email)
SELECT 'Jane Smith', 'jane@example.com'
WHERE NOT EXISTS (SELECT 1 FROM users WHERE email = 'jane@example.com');
INSERT INTO users (name, email)
SELECT 'Alice Johnson', 'alice@example.com'
WHERE NOT EXISTS (SELECT 1 FROM users WHERE email = 'alice@example.com');
-- Insert sample posts
INSERT INTO posts (title, content, user_id)
SELECT 'Welcome to PGlite', 'This is a sample post in your PGlite database!', 1
WHERE NOT EXISTS (SELECT 1 FROM posts WHERE title = 'Welcome to PGlite');
INSERT INTO posts (title, content, user_id)
SELECT 'Getting Started', 'PGlite is PostgreSQL compiled to WebAssembly.', 2
WHERE NOT EXISTS (SELECT 1 FROM posts WHERE title = 'Getting Started');
`);
console.log(chalk.green('✅ Database initialized with sample data'));
}
// Show usage help
function showHelp() {
console.log(`
${chalk.bold.cyan('🐘 Custom PGlite Socket Handler')} - ${chalk.cyan('Low-level socket implementation example')}
Usage:
node custom-handler.js [options]
Options:
--db=<path> Database path (default: memory://)
Examples:
memory:// - In-memory database
./data/mydb - Persistent file storage
/absolute/path/db - Absolute path
--dbname=<name> Custom database name (default: mydb)
--port=<port> Port to listen on (default: 5432)
--host=<host> Host to bind to (default: 127.0.0.1)
--debug=<level> Debug level 0-5 (default: 0)
--inspect Enable protocol inspection (prints raw data)
Examples:
# Start in-memory database with debug output
node custom-handler.js --db=memory:// --debug=1
# Start persistent database on custom port
node custom-handler.js --db=./data/mydb --port=5433
# Bind to all interfaces
node custom-handler.js --host=0.0.0.0
# Enable protocol inspection for debugging
node custom-handler.js --inspect
# Use a custom database name
node custom-handler.js --dbname=customdb
`);
}
// Main program entry point
if (process.argv.includes('--help') || process.argv.includes('-h')) {
showHelp();
} else {
startServer();
}