diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 3a18acca4..241d2967b 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -48,22 +48,47 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: pnpm + - uses: oven-sh/setup-bun@v2 + - name: Install working-directory: ${{ github.workspace }} run: pnpm install + - name: Get runtimes versions + run: node -v && npm -v && pnpm -v && bun -v + - name: Build all working-directory: ${{ github.workspace }} - env: - PGSRC: ${{ github.workspace }}/postgres-pglite run: | pnpm build:all + - name: Build server js + run: pnpm -C packages/pglite-socket/ build + + # - name: Setup tmate session + # uses: mxschmitt/action-tmate@v3 + + - name: Run server js with bun + run: bun packages/pglite-socket/dist/scripts/server.js + + - name: Run server js with node + run: node packages/pglite-socket/dist/scripts/server.js + + - name: Install vitest globally + run: pnpm i -g vitest tsx + + - name: Run server socket independently + run: tsx ./packages/pglite-socket/src/scripts/server.ts + + - name: Test socket server + run: vitest run ./packages/pglite-socket/tests/server.test.ts + - name: Typecheck pglite working-directory: ${{ github.workspace }}/packages/pglite run: pnpm typecheck + - name: Test pglite working-directory: ${{ github.workspace }}/packages/pglite run: pnpm test @@ -74,7 +99,7 @@ jobs: with: name: pglite-interim-build-files-node-v20.x path: ./packages/pglite/release/** - retention-days: 60 + retention-days: 30 - name: Upload pglite-tools build artifacts to Github artifacts id: upload-pglite-tools-release-files @@ -82,7 +107,7 @@ jobs: with: name: pglite-tools-release-files-node-v20.x path: ./packages/pglite-tools/release/** - retention-days: 60 + retention-days: 30 build-and-test-pglite: name: Build and Test packages/pglite @@ -279,8 +304,6 @@ jobs: - name: Build demo site env: - PGSRC: ${{ github.workspace }}/postgres-pglite - POSTGRES_PGLITE_OUT: ${{ github.workspace }}/packages/pglite/release PGLITE: ${{ github.workspace }}/packages/pglite working-directory: ${{ github.workspace }} run: | diff --git a/docs/extensions/extensions.data.ts b/docs/extensions/extensions.data.ts index de0fde1c5..02633b235 100644 --- a/docs/extensions/extensions.data.ts +++ b/docs/extensions/extensions.data.ts @@ -527,6 +527,20 @@ const baseExtensions: Extension[] = [ importName: 'uuid_ossp', size: 17936, }, + { + name: 'pgcrypto', + description: ` + The pgcrypto module provides cryptographic functions for PostgreSQL. + `, + shortDescription: + 'The pgcrypto module provides cryptographic functions for PostgreSQL.', + docs: 'https://www.postgresql.org/docs/current/pgcrypto.html', + tags: ['postgres extension', 'postgres/contrib'], + importPath: '@electric-sql/pglite/contrib/pgcrypto', + importName: 'pgcrypto', + core: true, + size: 1004373, + }, { name: 'pg_ivm', description: ` diff --git a/docs/repl/allExtensions.ts b/docs/repl/allExtensions.ts index 1cfb1a674..ab11a0250 100644 --- a/docs/repl/allExtensions.ts +++ b/docs/repl/allExtensions.ts @@ -19,6 +19,7 @@ export { pageinspect } from '@electric-sql/pglite/contrib/pageinspect' export { pg_buffercache } from '@electric-sql/pglite/contrib/pg_buffercache' export { pg_freespacemap } from '@electric-sql/pglite/contrib/pg_freespacemap' export { pg_surgery } from '@electric-sql/pglite/contrib/pg_surgery' +export { pgcrypto } from '@electric-sql/pglite/contrib/pgcrypto' export { pg_trgm } from '@electric-sql/pglite/contrib/pg_trgm' export { pg_visibility } from '@electric-sql/pglite/contrib/pg_visibility' export { pg_walinspect } from '@electric-sql/pglite/contrib/pg_walinspect' diff --git a/packages/pglite-socket/src/scripts/server.ts b/packages/pglite-socket/src/scripts/server.ts index 552295625..d30d8c4c7 100644 --- a/packages/pglite-socket/src/scripts/server.ts +++ b/packages/pglite-socket/src/scripts/server.ts @@ -1,325 +1,337 @@ -#!/usr/bin/env node - -import { PGlite, DebugLevel } from '@electric-sql/pglite' -import { PGLiteSocketServer } from '../index' -import { parseArgs } from 'node:util' -import { spawn, ChildProcess } from 'node:child_process' - -// Define command line argument options -const args = parseArgs({ - options: { - db: { - type: 'string', - short: 'd', - default: 'memory://', - help: 'Database path (relative or absolute). Use memory:// for in-memory database.', - }, - port: { - type: 'string', - short: 'p', - default: '5432', - help: 'Port to listen on', - }, - host: { - type: 'string', - short: 'h', - default: '127.0.0.1', - help: 'Host to bind to', - }, - path: { - type: 'string', - short: 'u', - default: undefined, - help: 'unix socket to bind to. Takes precedence over host:port', - }, - debug: { - type: 'string', - short: 'v', - default: '0', - help: 'Debug level (0-5)', - }, - run: { - type: 'string', - short: 'r', - default: undefined, - help: 'Command to run after server starts', - }, - 'include-database-url': { - type: 'boolean', - default: false, - help: 'Include DATABASE_URL in the environment of the subprocess', - }, - 'shutdown-timeout': { - type: 'string', - default: '5000', - help: 'Timeout in milliseconds for graceful subprocess shutdown (default: 5000)', - }, - help: { - type: 'boolean', - short: '?', - default: false, - help: 'Show help', - }, - }, -}) - -const help = `PGlite Socket Server -Usage: pglite-server [options] - -Options: - -d, --db=PATH Database path (default: memory://) - -p, --port=PORT Port to listen on (default: 5432) - -h, --host=HOST Host to bind to (default: 127.0.0.1) - -u, --path=UNIX Unix socket to bind to (default: undefined). Takes precedence over host:port - -v, --debug=LEVEL Debug level 0-5 (default: 0) - -r, --run=COMMAND Command to run after server starts - --include-database-url Include DATABASE_URL in subprocess environment - --shutdown-timeout=MS Timeout for graceful subprocess shutdown in ms (default: 5000) -` - -interface ServerConfig { - dbPath: string - port: number - host: string - path?: string - debugLevel: DebugLevel - runCommand?: string - includeDatabaseUrl: boolean - shutdownTimeout: number -} - -class PGLiteServerRunner { - private config: ServerConfig - private db: PGlite | null = null - private server: PGLiteSocketServer | null = null - private subprocessManager: SubprocessManager | null = null - - constructor(config: ServerConfig) { - this.config = config - } - - static parseConfig(): ServerConfig { - return { - dbPath: args.values.db as string, - port: parseInt(args.values.port as string, 10), - host: args.values.host as string, - path: args.values.path as string, - debugLevel: parseInt(args.values.debug as string, 10) as DebugLevel, - runCommand: args.values.run as string, - includeDatabaseUrl: args.values['include-database-url'] as boolean, - shutdownTimeout: parseInt(args.values['shutdown-timeout'] as string, 10), - } - } - - private createDatabaseUrl(): string { - const { host, port, path } = this.config - - if (path) { - // Unix socket connection - const socketDir = path.endsWith('/.s.PGSQL.5432') - ? path.slice(0, -13) - : path - return `postgresql://postgres:postgres@/postgres?host=${encodeURIComponent(socketDir)}` - } else { - // TCP connection - return `postgresql://postgres:postgres@${host}:${port}/postgres` - } - } - - private async initializeDatabase(): Promise { - console.log(`Initializing PGLite with database: ${this.config.dbPath}`) - console.log(`Debug level: ${this.config.debugLevel}`) - - this.db = new PGlite(this.config.dbPath, { debug: this.config.debugLevel }) - await this.db.waitReady - console.log('PGlite database initialized') - } - - private setupServerEventHandlers(): void { - if (!this.server || !this.subprocessManager) { - throw new Error('Server or subprocess manager not initialized') - } - - this.server.addEventListener('listening', (event) => { - const detail = ( - event as CustomEvent<{ port: number; host: string } | { host: string }> - ).detail - console.log(`PGLiteSocketServer listening on ${JSON.stringify(detail)}`) - - // Run the command after server starts listening - if (this.config.runCommand && this.subprocessManager) { - const databaseUrl = this.createDatabaseUrl() - this.subprocessManager.spawn( - this.config.runCommand, - databaseUrl, - this.config.includeDatabaseUrl, - ) - } - }) - - this.server.addEventListener('connection', (event) => { - const { clientAddress, clientPort } = ( - event as CustomEvent<{ clientAddress: string; clientPort: number }> - ).detail - console.log(`Client connected from ${clientAddress}:${clientPort}`) - }) - - this.server.addEventListener('error', (event) => { - const error = (event as CustomEvent).detail - console.error('Socket server error:', error) - }) - } - - private setupSignalHandlers(): void { - process.on('SIGINT', () => this.shutdown()) - process.on('SIGTERM', () => this.shutdown()) - } - - async start(): Promise { - try { - // Initialize database - await this.initializeDatabase() - - if (!this.db) { - throw new Error('Database initialization failed') - } - - // Create and setup the socket server - this.server = new PGLiteSocketServer({ - db: this.db, - port: this.config.port, - host: this.config.host, - path: this.config.path, - inspect: this.config.debugLevel > 0, - }) - - // Create subprocess manager - this.subprocessManager = new SubprocessManager((exitCode) => { - this.shutdown(exitCode) - }) - - // Setup event handlers - this.setupServerEventHandlers() - this.setupSignalHandlers() - - // Start the server - await this.server.start() - } catch (error) { - console.error('Failed to start PGLiteSocketServer:', error) - throw error - } - } - - async shutdown(exitCode: number = 0): Promise { - console.log('\nShutting down PGLiteSocketServer...') - - // Terminate subprocess if running - if (this.subprocessManager) { - this.subprocessManager.terminate(this.config.shutdownTimeout) - } - - // Stop server - if (this.server) { - await this.server.stop() - } - - // Close database - if (this.db) { - await this.db.close() - } - - console.log('Server stopped') - process.exit(exitCode) - } -} - -class SubprocessManager { - private childProcess: ChildProcess | null = null - private onExit: (code: number) => void - - constructor(onExit: (code: number) => void) { - this.onExit = onExit - } - - get process(): ChildProcess | null { - return this.childProcess - } - - spawn( - command: string, - databaseUrl: string, - includeDatabaseUrl: boolean, - ): void { - console.log(`Running command: ${command}`) - - // Prepare environment variables - const env = { ...process.env } - if (includeDatabaseUrl) { - env.DATABASE_URL = databaseUrl - console.log(`Setting DATABASE_URL=${databaseUrl}`) - } - - // Parse and spawn the command - const commandParts = command.trim().split(/\s+/) - this.childProcess = spawn(commandParts[0], commandParts.slice(1), { - env, - stdio: 'inherit', - }) - - this.childProcess.on('error', (error) => { - console.error('Error running command:', error) - // If subprocess fails to start, shutdown the server - console.log('Subprocess failed to start, shutting down...') - this.onExit(1) - }) - - this.childProcess.on('close', (code) => { - console.log(`Command exited with code ${code}`) - this.childProcess = null - - // If child process exits with non-zero code, notify parent - if (code !== null && code !== 0) { - console.log( - `Child process failed with exit code ${code}, shutting down...`, - ) - this.onExit(code) - } - }) - } - - terminate(timeout: number): void { - if (this.childProcess) { - console.log('Terminating child process...') - this.childProcess.kill('SIGTERM') - - // Give it a moment to exit gracefully, then force kill if needed - setTimeout(() => { - if (this.childProcess && !this.childProcess.killed) { - console.log('Force killing child process...') - this.childProcess.kill('SIGKILL') - } - }, timeout) - } - } -} - -// Main execution -async function main() { - // Show help and exit if requested - if (args.values.help) { - console.log(help) - process.exit(0) - } - - try { - const config = PGLiteServerRunner.parseConfig() - const serverRunner = new PGLiteServerRunner(config) - await serverRunner.start() - } catch (error) { - console.error('Unhandled error:', error) - process.exit(1) - } -} - -// Run the main function -main() +// #!/usr/bin/env node + +// import { PGlite, DebugLevel } from '@electric-sql/pglite' +// import { PGLiteSocketServer } from '../index' +// import { parseArgs } from 'node:util' +// import { spawn, ChildProcess } from 'node:child_process' + +// // Define command line argument options +// const args = parseArgs({ +// options: { +// db: { +// type: 'string', +// short: 'd', +// default: 'memory://', +// help: 'Database path (relative or absolute). Use memory:// for in-memory database.', +// }, +// port: { +// type: 'string', +// short: 'p', +// default: '5432', +// help: 'Port to listen on', +// }, +// host: { +// type: 'string', +// short: 'h', +// default: '127.0.0.1', +// help: 'Host to bind to', +// }, +// path: { +// type: 'string', +// short: 'u', +// default: undefined, +// help: 'unix socket to bind to. Takes precedence over host:port', +// }, +// debug: { +// type: 'string', +// short: 'v', +// default: '0', +// help: 'Debug level (0-5)', +// }, +// run: { +// type: 'string', +// short: 'r', +// default: undefined, +// help: 'Command to run after server starts', +// }, +// 'include-database-url': { +// type: 'boolean', +// default: false, +// help: 'Include DATABASE_URL in the environment of the subprocess', +// }, +// 'shutdown-timeout': { +// type: 'string', +// default: '5000', +// help: 'Timeout in milliseconds for graceful subprocess shutdown (default: 5000)', +// }, +// help: { +// type: 'boolean', +// short: '?', +// default: false, +// help: 'Show help', +// }, +// }, +// }) + +// const help = `PGlite Socket Server +// Usage: pglite-server [options] + +// Options: +// -d, --db=PATH Database path (default: memory://) +// -p, --port=PORT Port to listen on (default: 5432) +// -h, --host=HOST Host to bind to (default: 127.0.0.1) +// -u, --path=UNIX Unix socket to bind to (default: undefined). Takes precedence over host:port +// -v, --debug=LEVEL Debug level 0-5 (default: 0) +// -r, --run=COMMAND Command to run after server starts +// --include-database-url Include DATABASE_URL in subprocess environment +// --shutdown-timeout=MS Timeout for graceful subprocess shutdown in ms (default: 5000) +// ` + +// interface ServerConfig { +// dbPath: string +// port: number +// host: string +// path?: string +// debugLevel: DebugLevel +// runCommand?: string +// includeDatabaseUrl: boolean +// shutdownTimeout: number +// } + +// class PGLiteServerRunner { +// private config: ServerConfig +// private db: PGlite | null = null +// private server: PGLiteSocketServer | null = null +// private subprocessManager: SubprocessManager | null = null + +// constructor(config: ServerConfig) { +// this.config = config +// } + +// static parseConfig(): ServerConfig { +// return { +// dbPath: args.values.db as string, +// port: parseInt(args.values.port as string, 10), +// host: args.values.host as string, +// path: args.values.path as string, +// debugLevel: parseInt(args.values.debug as string, 10) as DebugLevel, +// runCommand: args.values.run as string, +// includeDatabaseUrl: args.values['include-database-url'] as boolean, +// shutdownTimeout: parseInt(args.values['shutdown-timeout'] as string, 10), +// } +// } + +// private createDatabaseUrl(): string { +// const { host, port, path } = this.config + +// if (path) { +// // Unix socket connection +// const socketDir = path.endsWith('/.s.PGSQL.5432') +// ? path.slice(0, -13) +// : path +// return `postgresql://postgres:postgres@/postgres?host=${encodeURIComponent(socketDir)}` +// } else { +// // TCP connection +// return `postgresql://postgres:postgres@${host}:${port}/postgres` +// } +// } + +// private async initializeDatabase(): Promise { +// console.log(`Initializing PGLite with database: ${this.config.dbPath}`) +// console.log(`Debug level: ${this.config.debugLevel}`) + +// this.db = await PGlite.create(this.config.dbPath, { debug: this.config.debugLevel }) +// console.log('PGlite database initialized') +// console.log('exiting now') +// process.exit(0) +// } + +// private setupServerEventHandlers(): void { +// if (!this.server || !this.subprocessManager) { +// throw new Error('Server or subprocess manager not initialized') +// } + +// this.server.addEventListener('listening', (event) => { +// const detail = ( +// event as CustomEvent<{ port: number; host: string } | { host: string }> +// ).detail +// console.log(`PGLiteSocketServer listening on ${JSON.stringify(detail)}`) + +// // Run the command after server starts listening +// if (this.config.runCommand && this.subprocessManager) { +// const databaseUrl = this.createDatabaseUrl() +// this.subprocessManager.spawn( +// this.config.runCommand, +// databaseUrl, +// this.config.includeDatabaseUrl, +// ) +// } +// }) + +// this.server.addEventListener('connection', (event) => { +// const { clientAddress, clientPort } = ( +// event as CustomEvent<{ clientAddress: string; clientPort: number }> +// ).detail +// console.log(`Client connected from ${clientAddress}:${clientPort}`) +// }) + +// this.server.addEventListener('error', (event) => { +// const error = (event as CustomEvent).detail +// console.error('Socket server error:', error) +// }) +// } + +// private setupSignalHandlers(): void { +// process.on('SIGINT', () => this.shutdown()) +// process.on('SIGTERM', () => this.shutdown()) +// } + +// async start(): Promise { +// try { +// // Initialize database +// await this.initializeDatabase() + +// if (!this.db) { +// throw new Error('Database initialization failed') +// } + +// // Create and setup the socket server +// this.server = new PGLiteSocketServer({ +// db: this.db, +// port: this.config.port, +// host: this.config.host, +// path: this.config.path, +// inspect: this.config.debugLevel > 0, +// }) + +// // Create subprocess manager +// this.subprocessManager = new SubprocessManager((exitCode) => { +// this.shutdown(exitCode) +// }) + +// // Setup event handlers +// this.setupServerEventHandlers() +// this.setupSignalHandlers() + +// // Start the server +// await this.server.start() +// } catch (error) { +// console.error('Failed to start PGLiteSocketServer:', error) +// throw error +// } +// } + +// async shutdown(exitCode: number = 0): Promise { +// console.log('\nShutting down PGLiteSocketServer...') + +// // Terminate subprocess if running +// if (this.subprocessManager) { +// this.subprocessManager.terminate(this.config.shutdownTimeout) +// } + +// // Stop server +// if (this.server) { +// await this.server.stop() +// } + +// // Close database +// if (this.db) { +// await this.db.close() +// } + +// console.log('Server stopped') +// process.exit(exitCode) +// } +// } + +// class SubprocessManager { +// private childProcess: ChildProcess | null = null +// private onExit: (code: number) => void + +// constructor(onExit: (code: number) => void) { +// this.onExit = onExit +// } + +// get process(): ChildProcess | null { +// return this.childProcess +// } + +// spawn( +// command: string, +// databaseUrl: string, +// includeDatabaseUrl: boolean, +// ): void { +// console.log(`Running command: ${command}`) + +// // Prepare environment variables +// const env = { ...process.env } +// if (includeDatabaseUrl) { +// env.DATABASE_URL = databaseUrl +// console.log(`Setting DATABASE_URL=${databaseUrl}`) +// } + +// // Parse and spawn the command +// const commandParts = command.trim().split(/\s+/) +// this.childProcess = spawn(commandParts[0], commandParts.slice(1), { +// env, +// stdio: 'inherit', +// }) + +// this.childProcess.on('error', (error) => { +// console.error('Error running command:', error) +// // If subprocess fails to start, shutdown the server +// console.log('Subprocess failed to start, shutting down...') +// this.onExit(1) +// }) + +// this.childProcess.on('close', (code) => { +// console.log(`Command exited with code ${code}`) +// this.childProcess = null + +// // If child process exits with non-zero code, notify parent +// if (code !== null && code !== 0) { +// console.log( +// `Child process failed with exit code ${code}, shutting down...`, +// ) +// this.onExit(code) +// } +// }) +// } + +// terminate(timeout: number): void { +// if (this.childProcess) { +// console.log('Terminating child process...') +// this.childProcess.kill('SIGTERM') + +// // Give it a moment to exit gracefully, then force kill if needed +// setTimeout(() => { +// if (this.childProcess && !this.childProcess.killed) { +// console.log('Force killing child process...') +// this.childProcess.kill('SIGKILL') +// } +// }, timeout) +// } +// } +// } + +// // Main execution +// async function main() { +// // Show help and exit if requested +// if (args.values.help) { +// console.log(help) +// process.exit(0) +// } + +// const pglite = await PGlite.create() +// const result = await pglite.exec('SELECT version();') +// console.log('server.ts dummy test: version ', result) + +// try { +// const config = PGLiteServerRunner.parseConfig() +// const serverRunner = new PGLiteServerRunner(config) +// await serverRunner.start() +// } catch (error) { +// console.error('Unhandled error:', error) +// process.exit(1) +// } +// } + +// // Run the main function +// main() + +import { PGlite } from '@electric-sql/pglite' + + PGlite.create({ debug: 2 }).then( async pg => { + const result = await pg.exec('SELECT version();') + console.log('sserver.ts dummy test: version ', result) + }) \ No newline at end of file diff --git a/packages/pglite-socket/tests/server.test.ts b/packages/pglite-socket/tests/server.test.ts index 4953b7ece..7d8ed8f70 100644 --- a/packages/pglite-socket/tests/server.test.ts +++ b/packages/pglite-socket/tests/server.test.ts @@ -3,6 +3,7 @@ import { spawn, ChildProcess } from 'node:child_process' import { createConnection } from 'net' import path from 'node:path' import { fileURLToPath } from 'node:url' +import { PGlite } from '@electric-sql/pglite' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const serverScript = path.resolve(__dirname, '../src/scripts/server.ts') @@ -39,35 +40,42 @@ describe('Server Script Tests', () => { } describe('Help and Basic Functionality', () => { - it('should show help when --help flag is used', async () => { - const serverProcess = spawn('tsx', [serverScript, '--help'], { - stdio: ['pipe', 'pipe', 'pipe'], - }) - - let output = '' - serverProcess.stdout?.on('data', (data) => { - output += data.toString() - }) - - await new Promise((resolve) => { - serverProcess.on('exit', (code) => { - expect(code).toBe(0) - expect(output).toContain('PGlite Socket Server') - expect(output).toContain('Usage:') - expect(output).toContain('Options:') - expect(output).toContain('--db') - expect(output).toContain('--port') - expect(output).toContain('--host') - resolve() - }) - }) + it('dummy', async () => { + const pglite = await PGlite.create({ debug: 2 }) + const result = await pglite.exec('SELECT version();') + console.log('dummy test: version ', result) }, 10000) + // it('should show help when --help flag is used', async () => { + // const serverProcess = spawn('npx', ['tsx', serverScript, '--help'], { + // stdio: ['pipe', 'pipe', 'pipe'], + // }) + + // let output = '' + // serverProcess.stdout?.on('data', (data) => { + // output += data.toString() + // }) + + // await new Promise((resolve) => { + // serverProcess.on('exit', (code) => { + // console.log(output) + // expect(code).toBe(0) + // expect(output).toContain('PGlite Socket Server') + // expect(output).toContain('Usage:') + // expect(output).toContain('Options:') + // expect(output).toContain('--db') + // expect(output).toContain('--port') + // expect(output).toContain('--host') + // resolve() + // }) + // }) + // }, 10000) + it('should accept and use debug level parameter', async () => { - const testPort = getTestPort() + // const testPort = getTestPort() const serverProcess = spawn( - 'tsx', - [serverScript, '--port', testPort.toString(), '--debug', '2'], + 'npx', + ['tsx', serverScript, '-u', '/tmp/.s.PGSQL.5432', '--debug', '2'], { stdio: ['pipe', 'pipe', 'pipe'], }, @@ -75,12 +83,20 @@ describe('Server Script Tests', () => { let output = '' serverProcess.stdout?.on('data', (data) => { + console.log(data.toString()) output += data.toString() }) + serverProcess.stderr?.on('data', (data) => { + console.error(data.toString()) + }) + // Wait for server to start - await waitForPort(testPort) + // await waitForPort(testPort) + await new Promise((resolve) => { + setTimeout(() => resolve, 5000) + }) // Kill the server serverProcess.kill('SIGTERM') @@ -91,123 +107,138 @@ describe('Server Script Tests', () => { }) }) }, 10000) - }) - - describe('Server Startup and Connectivity', () => { - let serverProcess: ChildProcess | null = null - - afterEach(async () => { - if (serverProcess) { - serverProcess.kill('SIGTERM') - await new Promise((resolve) => { - if (serverProcess) { - serverProcess.on('exit', () => resolve()) - } else { - resolve() - } - }) - serverProcess = null - } - }) - - it('should start server on TCP port and accept connections', async () => { - const testPort = getTestPort() - - serverProcess = spawn( - 'tsx', - [serverScript, '--port', testPort.toString()], - { - stdio: ['pipe', 'pipe', 'pipe'], - }, - ) - - let output = '' - serverProcess.stdout?.on('data', (data) => { - output += data.toString() - }) - - // Wait for server to be ready - const isReady = await waitForPort(testPort) - expect(isReady).toBe(true) - - // Check that we can connect - const socket = createConnection({ port: testPort, host: '127.0.0.1' }) - await new Promise((resolve, reject) => { - socket.on('connect', resolve) - socket.on('error', reject) - setTimeout(() => reject(new Error('Connection timeout')), 3000) - }) - socket.end() - - expect(output).toContain('PGlite database initialized') - expect(output).toContain(`"port":${testPort}`) - }, 10000) - - it('should work with memory database', async () => { - const testPort = getTestPort() - - serverProcess = spawn( - 'tsx', - [serverScript, '--port', testPort.toString(), '--db', 'memory://'], - { - stdio: ['pipe', 'pipe', 'pipe'], - }, - ) - - let output = '' - serverProcess.stdout?.on('data', (data) => { - output += data.toString() - }) - - const isReady = await waitForPort(testPort) - expect(isReady).toBe(true) - expect(output).toContain('Initializing PGLite with database: memory://') - }, 10000) - }) - - describe('Configuration Options', () => { - let serverProcess: ChildProcess | null = null - - afterEach(async () => { - if (serverProcess) { - serverProcess.kill('SIGTERM') - await new Promise((resolve) => { - if (serverProcess) { - serverProcess.on('exit', () => resolve()) - } else { - resolve() - } - }) - serverProcess = null - } - }) - - it('should handle different hosts', async () => { - const testPort = getTestPort() - - serverProcess = spawn( - 'tsx', - [serverScript, '--port', testPort.toString(), '--host', '0.0.0.0'], - { - stdio: ['pipe', 'pipe', 'pipe'], - }, - ) - - let output = '' - serverProcess.stdout?.on('data', (data) => { - output += data.toString() - }) - - const isReady = await waitForPort(testPort) - expect(isReady).toBe(true) - serverProcess.kill() - await new Promise((resolve) => { - serverProcess.on('exit', () => { - expect(output).toContain(`"host":"0.0.0.0"`) - serverProcess = null - resolve() - }) - }) - }, 10000) + // }) + + // describe('Server Startup and Connectivity', () => { + // let serverProcess: ChildProcess | null = null + + // afterEach(async () => { + // if (serverProcess) { + // serverProcess.kill('SIGTERM') + // await new Promise((resolve) => { + // if (serverProcess) { + // serverProcess.on('exit', () => resolve()) + // } else { + // resolve() + // } + // }) + // serverProcess = null + // } + // }) + + // it('should start server on TCP port and accept connections', async () => { + // const testPort = getTestPort() + + // serverProcess = spawn( + // 'npx', + // ['tsx', serverScript, '--port', testPort.toString()], + // { + // stdio: ['pipe', 'pipe', 'pipe'], + // }, + // ) + + // let output = '' + // serverProcess.stdout?.on('data', (data) => { + // console.log(data.toString()) + // output += data.toString() + // }) + + // serverProcess.stderr?.on('data', (data) => { + // console.error(data.toString()) + // }) + + // // Wait for server to be ready + // const isReady = await waitForPort(testPort) + // expect(isReady).toBe(true) + + // // Check that we can connect + // const socket = createConnection({ port: testPort, host: '127.0.0.1' }) + // await new Promise((resolve, reject) => { + // socket.on('connect', resolve) + // socket.on('error', reject) + // setTimeout(() => reject(new Error('Connection timeout')), 3000) + // }) + // socket.end() + + // expect(output).toContain('PGlite database initialized') + // expect(output).toContain(`"port":${testPort}`) + // }, 10000) + + // it('should work with memory database', async () => { + // const testPort = getTestPort() + + // serverProcess = spawn( + // 'npx', + // ['tsx', serverScript, '--port', testPort.toString(), '--db', 'memory://'], + // { + // stdio: ['pipe', 'pipe', 'pipe'], + // }, + // ) + + // let output = '' + // serverProcess.stdout?.on('data', (data) => { + // console.log(data.toString()) + // output += data.toString() + // }) + + // serverProcess.stderr?.on('data', (data) => { + // console.error(data.toString()) + // }) + + // const isReady = await waitForPort(testPort) + // expect(isReady).toBe(true) + // expect(output).toContain('Initializing PGLite with database: memory://') + // }, 10000) + // }) + + // describe('Configuration Options', () => { + // let serverProcess: ChildProcess | null = null + + // afterEach(async () => { + // if (serverProcess) { + // serverProcess.kill('SIGTERM') + // await new Promise((resolve) => { + // if (serverProcess) { + // serverProcess.on('exit', () => resolve()) + // } else { + // resolve() + // } + // }) + // serverProcess = null + // } + // }) + + // it('should handle different hosts', async () => { + // const testPort = getTestPort() + + // serverProcess = spawn( + // 'npx', + // ['tsx', serverScript, '--port', testPort.toString(), '--host', '0.0.0.0'], + // { + // stdio: ['pipe', 'pipe', 'pipe'], + // }, + // ) + + // let output = '' + // serverProcess.stdout?.on('data', (data) => { + // console.log(data.toString()) + // output += data.toString() + // }) + + // serverProcess.stderr?.on('data', (data) => { + // console.error(data.toString()) + // }) + + // const isReady = await waitForPort(testPort) + // expect(isReady).toBe(true) + // serverProcess.kill() + // await new Promise((resolve) => { + // serverProcess!.on('exit', () => { + // expect(output).toContain(`"host":"0.0.0.0"`) + // serverProcess = null + // resolve() + // }) + // }) + // }, 10000) }) }) diff --git a/packages/pglite/package.json b/packages/pglite/package.json index 8d7b326bd..f66ceae43 100644 --- a/packages/pglite/package.json +++ b/packages/pglite/package.json @@ -178,6 +178,7 @@ "bun": "^1.1.30", "concurrently": "^8.2.2", "http-server": "^14.1.1", + "openpgp": "^6.3.0", "playwright": "^1.48.0", "tinytar": "^0.1.0", "vitest": "^2.1.2" diff --git a/packages/pglite/src/contrib/pgcrypto.ts b/packages/pglite/src/contrib/pgcrypto.ts new file mode 100644 index 000000000..063d87dfc --- /dev/null +++ b/packages/pglite/src/contrib/pgcrypto.ts @@ -0,0 +1,16 @@ +import type { + Extension, + ExtensionSetupResult, + PGliteInterface, +} from '../interface' + +const setup = async (_pg: PGliteInterface, _emscriptenOpts: any) => { + return { + bundlePath: new URL('../../release/pgcrypto.tar.gz', import.meta.url), + } satisfies ExtensionSetupResult +} + +export const pgcrypto = { + name: 'pgcrypto', + setup, +} satisfies Extension diff --git a/packages/pglite/tests/contrib/pgcrypto.test.js b/packages/pglite/tests/contrib/pgcrypto.test.js new file mode 100644 index 000000000..817c7963a --- /dev/null +++ b/packages/pglite/tests/contrib/pgcrypto.test.js @@ -0,0 +1,117 @@ +import { describe, it, expect } from 'vitest' +import { PGlite } from '../../dist/index.js' +import { pgcrypto } from '../../dist/contrib/pgcrypto.js' +import * as openpgp from 'openpgp' + +describe('pg_pgcryptotrgm', () => { + it('digest', async () => { + const pg = new PGlite({ + extensions: { + pgcrypto, + }, + }) + + await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;') + + const res = await pg.query( + "SELECT encode(digest(convert_to('test', 'UTF8'), 'sha1'), 'hex') as value;", + ) + expect(res.rows[0].value, 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3') + }) + + it('hmac', async () => { + const pg = new PGlite({ + extensions: { + pgcrypto, + }, + }) + + await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;') + + const res = await pg.query( + "SELECT encode(hmac(convert_to('test', 'UTF8'), convert_to('key', 'UTF8'), 'sha1'), 'hex') as value;", + ) + expect(res.rows[0].value).toEqual( + '671f54ce0c540f78ffe1e26dcf9c2a047aea4fda', + ) + }) + + it('crypt', async () => { + const pg = new PGlite({ + extensions: { + pgcrypto, + }, + }) + + await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;') + + const res = await pg.query("SELECT crypt('test', gen_salt('bf')) as value;") + expect(res.rows[0].value.length).toEqual(60) + }) + + it('gen_salt', async () => { + const pg = new PGlite({ + extensions: { + pgcrypto, + }, + }) + + await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;') + + const res = await pg.query("SELECT gen_salt('bf') as value;") + expect(res.rows[0].value.length).toEqual(29) + }) + + it('pgp_sym_encrypt and pgp_sym_decrypt', async () => { + const pg = new PGlite({ + extensions: { + pgcrypto, + }, + }) + + await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;') + + const res = await pg.query( + "SELECT pgp_sym_encrypt('test', 'key') as value;", + ) + const encrypted = res.rows[0].value + + const res2 = await pg.query("SELECT pgp_sym_decrypt($1, 'key') as value;", [ + encrypted, + ]) + expect(res2.rows[0].value).toEqual('test') + }) + + it('pgp_pub_encrypt and pgp_pub_decrypt', async () => { + const pg = new PGlite({ + extensions: { + pgcrypto, + }, + }) + + await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;') + + const { privateKey, publicKey } = await openpgp.generateKey({ + type: 'rsa', + rsaBits: 2048, + userIDs: [{ name: 'PGlite', email: 'hello@pglite.dev' }], + passphrase: '', + }) + + const toEncrypt = 'PGlite@$#%!^$&*WQFgjqPkVERewfreg094340f1012-=' + + const e2 = await pg.exec( + ` +WITH encrypted AS ( + SELECT pgp_pub_encrypt('${toEncrypt}', dearmor('${publicKey}')) AS encrypted +) +SELECT + pgp_pub_decrypt(encrypted, dearmor('${privateKey}')) as decrypted_output +FROM encrypted; +`, + ) + expect(e2[0].rows[0].decrypted_output, toEncrypt) + }) +}) + +// TODO: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4fe1e8540..1693c8901 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -189,6 +189,9 @@ importers: http-server: specifier: ^14.1.1 version: 14.1.1 + openpgp: + specifier: ^6.3.0 + version: 6.3.0 playwright: specifier: ^1.48.0 version: 1.48.0 @@ -3384,6 +3387,10 @@ packages: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true + openpgp@6.3.0: + resolution: {integrity: sha512-pLzCU8IgyKXPSO11eeharQkQ4GzOKNWhXq79pQarIRZEMt1/ssyr+MIuWBv1mNoenJLg04gvPx+fi4gcKZ4bag==} + engines: {node: '>= 18.0.0'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -8008,6 +8015,8 @@ snapshots: opener@1.5.2: {} + openpgp@6.3.0: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 diff --git a/postgres-pglite b/postgres-pglite index 1195d5388..f618c3c2a 160000 --- a/postgres-pglite +++ b/postgres-pglite @@ -1 +1 @@ -Subproject commit 1195d5388bd5529e0013c45fa816cfcd953d84e0 +Subproject commit f618c3c2a5ace54e564ab980578f2fc6f9ab8913