@rineex/pg-slonik
v0.3.0
Published
PostgreSQL adapter using Slonik for Rineex core modules
Maintainers
Readme
@rineex/pg-slonik
NestJS module for PostgreSQL using Slonik. Provides type-safe database connection management with dependency injection, automatic retry logic, and graceful shutdown handling.
Table of Contents
Overview
@rineex/pg-slonik is a NestJS module wrapper for
Slonik, providing seamless integration with
NestJS dependency injection, automatic connection retry logic, and graceful
shutdown handling.
Key Features
- NestJS Integration: Full NestJS module support with dependency injection
- Multiple Connections: Support for multiple named database connections
- Type-Safe Database Access: Full TypeScript support with Slonik's type system
- Automatic Retry Logic: Built-in connection retry with configurable attempts and delays
- Graceful Shutdown: Automatic pool cleanup on application shutdown
- SQL Safety: Built-in SQL injection protection via Slonik
- Transaction Support: First-class transaction handling
- Production Ready: Used in high-performance systems at scale
Installation
npm install @rineex/pg-slonik
# or
pnpm add @rineex/pg-slonik
# or
yarn add @rineex/pg-slonikRequirements
- Node.js: 18.0 or higher
- TypeScript: 5.0 or higher (recommended: 5.9+)
- PostgreSQL: 12.0 or higher
- @nestjs/common: 11.0 or higher
Quick Start
Here's a minimal example to get started with NestJS:
import { Module } from '@nestjs/common';
import { SlonikModule } from '@rineex/pg-slonik';
@Module({
imports: [
SlonikModule.register({
connections: [
{
name: 'DEFAULT',
dsn: 'postgresql://user:password@localhost:5432/database',
},
],
}),
],
})
export class AppModule {}Then inject the pool in your service:
import { Injectable } from '@nestjs/common';
import { InjectPool } from '@rineex/pg-slonik';
import type { DatabasePool } from 'slonik';
@Injectable()
export class UserService {
constructor(@InjectPool() private readonly pool: DatabasePool) {}
async findUser(id: string) {
const result = await this.pool.query(
this.pool.sql`SELECT * FROM users WHERE id = ${id}`,
);
return result.rows[0];
}
}API Reference
SlonikModule
NestJS module for managing Slonik database connections.
SlonikModule.register(options: SlonikModuleOptions): DynamicModule
Registers the Slonik module with the provided configuration.
Parameters:
options.connections(required): Array of connection configurationsconnection.name(required): Unique name for the connection (default: 'DEFAULT')connection.dsn(required): PostgreSQL connection string (DSN)connection.options(optional): SlonikClientConfigurationInputoptionsconnection.tags(optional): Array of connection tags
options.isGlobal(optional): Whether the module should be global (default: true)
Returns: A configured NestJS DynamicModule
InjectPool(name?: string)
Decorator for injecting a Slonik database pool into a class.
Parameters:
name(optional): Name of the connection to inject (default: 'DEFAULT')
Returns: A NestJS Inject decorator
Example:
@Injectable()
export class MyService {
constructor(@InjectPool('DEFAULT') private readonly pool: DatabasePool) {}
}Type Exports
SlonikConnectionConfig: Configuration interface for a single connectionSlonikModuleOptions: Module configuration interfaceSlonikModuleExtraOptions: Additional module options (e.g.,isGlobal)
Examples
Basic Module Setup
import { Module } from '@nestjs/common';
import { SlonikModule } from '@rineex/pg-slonik';
@Module({
imports: [
SlonikModule.register({
connections: [
{
name: 'DEFAULT',
dsn: process.env.DATABASE_URL!,
},
],
}),
],
})
export class AppModule {}Multiple Connections
import { Module } from '@nestjs/common';
import { SlonikModule } from '@rineex/pg-slonik';
@Module({
imports: [
SlonikModule.register({
connections: [
{
name: 'DEFAULT',
dsn: process.env.DATABASE_URL!,
},
{
name: 'ANALYTICS',
dsn: process.env.ANALYTICS_DATABASE_URL!,
options: {
maximumPoolSize: 5,
statementTimeout: 30000,
},
},
],
}),
],
})
export class AppModule {}Service with Dependency Injection
import { Injectable } from '@nestjs/common';
import { InjectPool } from '@rineex/pg-slonik';
import type { DatabasePool } from 'slonik';
@Injectable()
export class UserService {
constructor(@InjectPool() private readonly pool: DatabasePool) {}
async findAll() {
const result = await this.pool.query(
this.pool.sql`SELECT id, email FROM users LIMIT 10`,
);
return result.rows;
}
async findById(id: string) {
const result = await this.pool.query(
this.pool.sql`SELECT * FROM users WHERE id = ${id}`,
);
return result.rows[0] || null;
}
}Using Named Connections
import { Injectable } from '@nestjs/common';
import { InjectPool } from '@rineex/pg-slonik';
import type { DatabasePool } from 'slonik';
@Injectable()
export class AnalyticsService {
constructor(
@InjectPool('ANALYTICS') private readonly analyticsPool: DatabasePool,
) {}
async getMetrics() {
const result = await this.analyticsPool.query(
this.analyticsPool.sql`SELECT * FROM metrics ORDER BY created_at DESC`,
);
return result.rows;
}
}Transaction Handling
import { Injectable } from '@nestjs/common';
import { InjectPool } from '@rineex/pg-slonik';
import type { DatabasePool } from 'slonik';
@Injectable()
export class UserService {
constructor(@InjectPool() private readonly pool: DatabasePool) {}
async createUserWithProfile(userId: string, email: string, name: string) {
await this.pool.transaction(async transactionConnection => {
await transactionConnection.query(
transactionConnection.sql`
INSERT INTO users (id, email) VALUES (${userId}, ${email})
`,
);
await transactionConnection.query(
transactionConnection.sql`
INSERT INTO user_profiles (user_id, name) VALUES (${userId}, ${name})
`,
);
});
}
}Repository Pattern
import { Injectable } from '@nestjs/common';
import { InjectPool } from '@rineex/pg-slonik';
import type { DatabasePool } from 'slonik';
interface User {
id: string;
email: string;
createdAt: Date;
}
@Injectable()
export class UserRepository {
constructor(@InjectPool() private readonly pool: DatabasePool) {}
async findById(id: string) {
const result = await this.pool.query(
this.pool.sql`
SELECT * FROM users WHERE id = ${id}
`,
);
if (result.rows.length === 0) {
return null;
}
// Map database row to domain object
return this.mapToUser(result.rows[0]);
}
async save(user: User) {
await this.pool.query(
this.pool.sql`
INSERT INTO users (id, email, created_at)
VALUES (${user.id}, ${user.email}, ${user.createdAt})
ON CONFLICT (id) DO UPDATE
SET email = ${user.email}
`,
);
}
private mapToUser(row: any): User {
return {
id: row.id,
email: row.email,
createdAt: row.created_at,
};
}
}Advanced Configuration
import { Module } from '@nestjs/common';
import { SlonikModule } from '@rineex/pg-slonik';
@Module({
imports: [
SlonikModule.register({
connections: [
{
name: 'DEFAULT',
dsn: process.env.DATABASE_URL!,
options: {
maximumPoolSize: 20,
idleTimeout: 5000,
statementTimeout: 60000,
connectionTimeout: 10000,
interceptors: [
// Add custom interceptors here
],
},
tags: ['primary', 'read-write'],
},
],
isGlobal: true, // Make the module global
}),
],
})
export class AppModule {}Best Practices
1. Use Dependency Injection
Always inject the pool using the @InjectPool() decorator instead of creating
pools manually:
// ❌ BAD: Creating pools manually
const pool = await createPool(connectionString);
// ✅ GOOD: Using dependency injection
@Injectable()
export class MyService {
constructor(@InjectPool() private readonly pool: DatabasePool) {}
}2. Automatic Pool Cleanup
The module automatically closes all connection pools on application shutdown. You don't need to manually handle cleanup:
// ✅ GOOD: Automatic cleanup via OnApplicationShutdown
// The SlonikModule handles this automatically3. Use Tagged Template Literals
Always use Slonik's sql tagged template literal for queries to prevent SQL
injection:
// ❌ BAD: String concatenation (SQL injection risk)
const query = `SELECT * FROM users WHERE id = '${userId}'`;
// ✅ GOOD: Tagged template literal
const query = this.pool.sql`SELECT * FROM users WHERE id = ${userId}`;4. Handle Transactions Properly
Use transactions for operations that must be atomic:
await this.pool.transaction(async transactionConnection => {
// All queries in this block are part of the same transaction
await transactionConnection.query(/* ... */);
await transactionConnection.query(/* ... */);
// Transaction commits automatically if no errors
});5. Map Database Rows to Domain Objects
Keep database concerns separate from domain logic:
// ❌ BAD: Exposing database rows directly
async getUser(id: string) {
const result = await this.pool.query(/* ... */);
return result.rows[0]; // Raw database row
}
// ✅ GOOD: Mapping to domain objects
async getUser(id: string) {
const result = await this.pool.query(/* ... */);
return this.mapToUser(result.rows[0]); // Domain object
}6. Use Named Connections for Multiple Databases
When working with multiple databases, use named connections:
// ✅ GOOD: Named connections for different databases
SlonikModule.register({
connections: [
{ name: 'DEFAULT', dsn: process.env.DATABASE_URL! },
{ name: 'ANALYTICS', dsn: process.env.ANALYTICS_DB_URL! },
],
});
// Then inject the specific connection
@InjectPool('ANALYTICS') private readonly analyticsPool: DatabasePool7. Configure Connection Options
Use the options field to configure pool behavior:
SlonikModule.register({
connections: [
{
name: 'DEFAULT',
dsn: process.env.DATABASE_URL!,
options: {
maximumPoolSize: 20,
statementTimeout: 60000,
// Add other Slonik configuration options
},
},
],
});Contributing
Contributions are welcome! Please follow these guidelines:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Write tests for new functionality
- Ensure all tests pass (
pnpm test) - Follow the code style (
pnpm lint) - Commit with clear messages
- Push to the branch and create a Pull Request
Development Setup
# Install dependencies
pnpm install
# Run tests
pnpm test
# Run linter
pnpm lint
# Check types
pnpm check-types
# Build the package
pnpm buildLicense
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
Related Resources
Support
For issues, questions, or suggestions, please open an issue on GitHub.
Made with ❤️ by the Rineex Team
