@bernierllc/database-adapter-postgresql 1.0.0

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/README.md ADDED
@@ -0,0 +1,456 @@
1
+ # @bernierllc/database-adapter-postgresql
2
+
3
+ PostgreSQL/Supabase database adapter with Row Level Security, real-time subscriptions, and full-text search.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @bernierllc/database-adapter-postgresql
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - **Connection Pooling** - Efficient connection management with pg Pool
14
+ - **Row Level Security** - Multi-tenant security with Clerk JWT integration
15
+ - **Real-time Subscriptions** - Supabase real-time for collaborative editing
16
+ - **Full-Text Search** - pg_trgm fuzzy matching and ts_vector search
17
+ - **Transaction Support** - ACID transactions with savepoints
18
+ - **Automatic Retries** - Connection retry with exponential backoff
19
+ - **Type Safety** - Strict TypeScript with comprehensive type definitions
20
+ - **Logging Integration** - Structured logging with @bernierllc/logger
21
+
22
+ ## Usage
23
+
24
+ ### Basic Connection
25
+
26
+ ```typescript
27
+ import { PostgreSQLAdapter } from '@bernierllc/database-adapter-postgresql';
28
+
29
+ const adapter = new PostgreSQLAdapter({
30
+ type: 'postgresql',
31
+ host: 'localhost',
32
+ port: 5432,
33
+ database: 'myapp',
34
+ username: 'postgres',
35
+ password: 'password',
36
+ pooling: {
37
+ min: 2,
38
+ max: 10
39
+ }
40
+ });
41
+
42
+ await adapter.connect();
43
+ ```
44
+
45
+ ### With Supabase
46
+
47
+ ```typescript
48
+ const adapter = new PostgreSQLAdapter({
49
+ type: 'postgresql',
50
+ host: 'db.project.supabase.co',
51
+ port: 5432,
52
+ database: 'postgres',
53
+ username: 'postgres',
54
+ password: 'password',
55
+ ssl: true,
56
+ supabaseUrl: 'https://project.supabase.co',
57
+ supabaseAnonKey: 'your-anon-key'
58
+ });
59
+ ```
60
+
61
+ ### Query Execution
62
+
63
+ ```typescript
64
+ // Simple query
65
+ const result = await adapter.query('SELECT * FROM users WHERE id = $1', ['user-id']);
66
+
67
+ // Typed query
68
+ interface User {
69
+ id: string;
70
+ email: string;
71
+ role: string;
72
+ }
73
+
74
+ const users = await adapter.query<User>('SELECT * FROM users');
75
+ console.log(users.rows[0].email);
76
+ ```
77
+
78
+ ### Transactions
79
+
80
+ ```typescript
81
+ const txn = await adapter.beginTransaction();
82
+
83
+ try {
84
+ await txn.query('INSERT INTO users (email) VALUES ($1)', ['user@example.com']);
85
+ await txn.query('INSERT INTO profiles (user_id) VALUES ($1)', ['user-id']);
86
+
87
+ await txn.commit();
88
+ } catch (error) {
89
+ await txn.rollback();
90
+ throw error;
91
+ }
92
+ ```
93
+
94
+ ### Savepoints
95
+
96
+ ```typescript
97
+ const txn = await adapter.beginTransaction();
98
+
99
+ await txn.query('INSERT INTO content (title) VALUES ($1)', ['Draft 1']);
100
+ await txn.savepoint('draft1');
101
+
102
+ await txn.query('INSERT INTO content (title) VALUES ($1)', ['Draft 2']);
103
+
104
+ // Rollback to savepoint
105
+ await txn.rollbackToSavepoint('draft1');
106
+
107
+ await txn.commit(); // Only 'Draft 1' is committed
108
+ ```
109
+
110
+ ### Row Level Security
111
+
112
+ ```typescript
113
+ import { RLSPolicyGenerator } from '@bernierllc/database-adapter-postgresql';
114
+
115
+ // Generate user-scoped policy
116
+ const policy = RLSPolicyGenerator.generateUserPolicy('content');
117
+ await adapter.query(policy);
118
+
119
+ // Set RLS context for queries
120
+ adapter.setRLSContext('user_123', 'user', 'tenant_456');
121
+
122
+ // All subsequent queries respect RLS policies
123
+ const content = await adapter.query('SELECT * FROM content');
124
+ // Only returns content for user_123 in tenant_456
125
+ ```
126
+
127
+ ### Clerk Integration
128
+
129
+ ```typescript
130
+ import { ClerkIntegration } from '@bernierllc/database-adapter-postgresql';
131
+
132
+ // Set up Clerk authentication functions
133
+ const setup = ClerkIntegration.generateCompleteSetup();
134
+ await adapter.query(setup);
135
+
136
+ // RLS policies automatically use Clerk JWT
137
+ const policy = RLSPolicyGenerator.generateUserPolicy('content');
138
+ await adapter.query(policy);
139
+ ```
140
+
141
+ ### Real-time Subscriptions
142
+
143
+ ```typescript
144
+ import { RealtimeManager } from '@bernierllc/database-adapter-postgresql';
145
+
146
+ const realtimeManager = new RealtimeManager(adapter);
147
+
148
+ // Subscribe to content changes
149
+ const unsubscribe = realtimeManager.subscribeToContent('content-id', {
150
+ onInsert: (payload) => {
151
+ console.log('New version created:', payload.new);
152
+ },
153
+ onUpdate: (payload) => {
154
+ console.log('Content updated:', payload.new);
155
+ },
156
+ onDelete: (payload) => {
157
+ console.log('Content deleted:', payload.old);
158
+ }
159
+ });
160
+
161
+ // Broadcast presence for collaborative editing
162
+ await realtimeManager.broadcastPresence('content-id', 'user-id', {
163
+ line: 10,
164
+ column: 5
165
+ });
166
+
167
+ // Get current presence state
168
+ const presence = realtimeManager.getPresence('content-id');
169
+
170
+ // Cleanup
171
+ unsubscribe();
172
+ ```
173
+
174
+ ### Full-Text Search
175
+
176
+ ```typescript
177
+ import { FullTextSearchManager } from '@bernierllc/database-adapter-postgresql';
178
+
179
+ const searchManager = new FullTextSearchManager(adapter);
180
+
181
+ // Create indexes first
182
+ await searchManager.createIndexes();
183
+
184
+ // Fuzzy search with typo tolerance
185
+ const results = await searchManager.searchContent('databse', {
186
+ minSimilarity: 0.3,
187
+ limit: 10
188
+ });
189
+
190
+ results.forEach(result => {
191
+ console.log(result.title, 'Relevance:', result.totalSimilarity);
192
+ });
193
+
194
+ // Combined search (pg_trgm + ts_vector)
195
+ const combined = await searchManager.searchCombined('PostgreSQL performance', {
196
+ minSimilarity: 0.2,
197
+ limit: 20
198
+ });
199
+
200
+ // Search specific fields
201
+ const titleResults = await searchManager.searchContent('guide', {
202
+ fields: ['title'],
203
+ minSimilarity: 0.4
204
+ });
205
+ ```
206
+
207
+ ### Migrations
208
+
209
+ ```typescript
210
+ import { migration_001_initial_schema } from '@bernierllc/database-adapter-postgresql';
211
+
212
+ // Run migration
213
+ await migration_001_initial_schema.up(adapter);
214
+
215
+ // Rollback
216
+ await migration_001_initial_schema.down(adapter);
217
+ ```
218
+
219
+ ### Health Checks
220
+
221
+ ```typescript
222
+ const health = await adapter.healthCheck();
223
+
224
+ console.log('Connected:', health.connected);
225
+ console.log('Latency:', health.latencyMs, 'ms');
226
+ console.log('Version:', health.version);
227
+ console.log('Active connections:', health.activeConnections);
228
+ console.log('Max connections:', health.maxConnections);
229
+ ```
230
+
231
+ ### Pool Statistics
232
+
233
+ ```typescript
234
+ const stats = adapter.getPoolStats();
235
+
236
+ console.log('Total connections:', stats.total);
237
+ console.log('Idle connections:', stats.idle);
238
+ console.log('Waiting requests:', stats.waiting);
239
+ ```
240
+
241
+ ## API Reference
242
+
243
+ ### PostgreSQLAdapter
244
+
245
+ Main adapter class extending `BaseDatabaseAdapter` from `@bernierllc/database-adapter-core`.
246
+
247
+ #### Methods
248
+
249
+ - `connect(): Promise<void>` - Connect to PostgreSQL with retry
250
+ - `disconnect(): Promise<void>` - Close connection pool
251
+ - `query<T>(sql: string, params?: any[]): Promise<IQueryResult<T>>` - Execute query
252
+ - `beginTransaction(): Promise<ITransaction>` - Start transaction
253
+ - `healthCheck(): Promise<IDatabaseHealth>` - Check connection health
254
+ - `setRLSContext(userId: string, userRole: string, tenantId?: string): void` - Set RLS context
255
+ - `clearRLSContext(): void` - Clear RLS context
256
+ - `getSupabaseClient(): SupabaseClient | null` - Get Supabase client
257
+ - `getPoolStats(): object | null` - Get connection pool statistics
258
+
259
+ ### PostgreSQLTransaction
260
+
261
+ Transaction implementation with savepoint support.
262
+
263
+ #### Methods
264
+
265
+ - `query<T>(sql: string, params?: any[]): Promise<IQueryResult<T>>` - Execute query in transaction
266
+ - `commit(): Promise<void>` - Commit transaction
267
+ - `rollback(): Promise<void>` - Rollback transaction
268
+ - `savepoint(name: string): Promise<void>` - Create savepoint
269
+ - `rollbackToSavepoint(name: string): Promise<void>` - Rollback to savepoint
270
+ - `releaseSavepoint(name: string): Promise<void>` - Release savepoint
271
+ - `isActive(): boolean` - Check if transaction is active
272
+
273
+ ### RLSPolicyGenerator
274
+
275
+ Static utility for generating RLS policies.
276
+
277
+ #### Methods
278
+
279
+ - `generateUserPolicy(table: string): string` - User-scoped access
280
+ - `generateAdminPolicy(table: string): string` - Admin bypass
281
+ - `generateTenantPolicy(table: string): string` - Multi-tenant isolation
282
+ - `generatePublicReadPolicy(table: string, condition?: string): string` - Public read access
283
+ - `generateSharedResourcePolicy(table: string, sharingTable?: string): string` - Shared resources
284
+ - `generateRoleBasedPolicy(table: string, roles: string[]): string` - Role-based access
285
+ - `generateTimeBasedPolicy(table: string): string` - Time-based access
286
+ - `disableRLS(table: string): string` - Disable RLS (use with caution)
287
+
288
+ ### ClerkIntegration
289
+
290
+ Static utility for Clerk JWT integration.
291
+
292
+ #### Methods
293
+
294
+ - `generateCompleteSetup(): string` - Generate all functions and schema
295
+ - `generateAuthSchema(): string` - Create auth schema
296
+ - `generateClerkUserIdFunction(): string` - Extract Clerk user ID
297
+ - `generateUserIdFunction(): string` - Get internal user ID
298
+ - `generateIsAdminFunction(): string` - Check admin status
299
+ - `generateUserRoleFunction(): string` - Get user role
300
+ - `generateHasRoleFunction(): string` - Check specific role
301
+ - `generateUserTenantIdFunction(): string` - Get user tenant ID
302
+
303
+ ### RealtimeManager
304
+
305
+ Manages Supabase real-time subscriptions.
306
+
307
+ #### Methods
308
+
309
+ - `subscribeToContent(contentId: string, callbacks: RealtimeCallbacks): () => void` - Subscribe to content
310
+ - `subscribeToTable(table: string, filter: string | null, callbacks: RealtimeCallbacks): () => void` - Subscribe to table
311
+ - `broadcastPresence(contentId: string, userId: string, cursorPosition?: object): Promise<void>` - Broadcast presence
312
+ - `getPresence(contentId: string): object | null` - Get presence state
313
+ - `unsubscribe(contentId: string): Promise<void>` - Unsubscribe from channel
314
+ - `unsubscribeAll(): Promise<void>` - Unsubscribe from all channels
315
+
316
+ ### FullTextSearchManager
317
+
318
+ PostgreSQL full-text search with pg_trgm.
319
+
320
+ #### Methods
321
+
322
+ - `searchContent(query: string, options?: SearchOptions): Promise<SearchResult[]>` - Fuzzy search
323
+ - `searchWithTsVector(query: string, options?: SearchOptions): Promise<any[]>` - ts_vector search
324
+ - `searchCombined(query: string, options?: SearchOptions): Promise<SearchResult[]>` - Combined search
325
+ - `createIndexes(): Promise<void>` - Create search indexes
326
+ - `dropIndexes(): Promise<void>` - Drop search indexes
327
+ - `getSearchStats(): Promise<object>` - Get index statistics
328
+
329
+ ## Configuration
330
+
331
+ ### IDatabaseConfig
332
+
333
+ ```typescript
334
+ interface IDatabaseConfig {
335
+ type: 'postgresql';
336
+ host: string;
337
+ port?: number; // Default: 5432
338
+ database: string;
339
+ username: string;
340
+ password: string;
341
+ ssl?: boolean | object;
342
+ pooling?: {
343
+ min?: number; // Default: 2
344
+ max?: number; // Default: 10
345
+ idleTimeoutMs?: number; // Default: 30000
346
+ };
347
+ connectionTimeoutMs?: number; // Default: 5000
348
+ }
349
+ ```
350
+
351
+ ### ISupabaseConfig
352
+
353
+ ```typescript
354
+ interface ISupabaseConfig {
355
+ supabaseUrl?: string;
356
+ supabaseAnonKey?: string;
357
+ supabaseServiceKey?: string;
358
+ }
359
+ ```
360
+
361
+ ## Integration Status
362
+
363
+ - **Logger**: ✅ Integrated - Uses @bernierllc/logger for structured logging
364
+ - **Docs-Suite**: ✅ Ready - TypeDoc API documentation, complete README
365
+ - **NeverHub**: 🔔 Optional - Health metrics and pool statistics publishing
366
+
367
+ ### NeverHub Integration
368
+
369
+ This package provides **optional** NeverHub integration for enhanced observability:
370
+
371
+ **What NeverHub Provides:**
372
+ - Real-time connection pool monitoring
373
+ - Query performance metrics aggregation
374
+ - Database health status broadcasting
375
+ - Automatic service discovery for database adapters
376
+
377
+ **Integration Approach:**
378
+ - **Graceful Degradation**: All core functionality works without NeverHub
379
+ - **Auto-Detection**: Package detects NeverHub availability at runtime
380
+ - **Opt-In Publishing**: Metrics are published only when NeverHub is present
381
+
382
+ **Usage Example:**
383
+ ```typescript
384
+ import { PostgreSQLAdapter } from '@bernierllc/database-adapter-postgresql';
385
+ import { NeverHubAdapter } from '@bernierllc/neverhub-adapter';
386
+
387
+ const adapter = new PostgreSQLAdapter({ /* config */ });
388
+ await adapter.connect();
389
+
390
+ // Optional: Publish metrics to NeverHub if available
391
+ if (await NeverHubAdapter.detect()) {
392
+ const neverhub = new NeverHubAdapter();
393
+ await neverhub.register({
394
+ type: 'database-adapter',
395
+ name: '@bernierllc/database-adapter-postgresql',
396
+ capabilities: [
397
+ { type: 'storage', name: 'postgresql', version: '1.0.0' }
398
+ ]
399
+ });
400
+
401
+ // Publish pool statistics
402
+ setInterval(async () => {
403
+ const stats = adapter.getPoolStats();
404
+ await neverhub.publishEvent({
405
+ type: 'database.pool.stats',
406
+ data: stats
407
+ });
408
+ }, 30000); // Every 30 seconds
409
+ }
410
+ ```
411
+
412
+ **Metrics Published:**
413
+ - `database.pool.stats` - Connection pool statistics (total, idle, waiting)
414
+ - `database.health` - Health check results (connected, latency, version)
415
+ - `database.query.performance` - Query execution metrics (optional, requires instrumentation)
416
+
417
+ **Why Optional:**
418
+ - Core packages should minimize dependencies
419
+ - Database operations must work in all environments
420
+ - NeverHub primarily benefits distributed systems with multiple services
421
+ - Single-instance applications don't need service discovery
422
+
423
+ ## Performance
424
+
425
+ - **Query Latency**: <5ms (p95) for indexed queries
426
+ - **Connection Pool Reuse**: >90%
427
+ - **Real-time Latency**: <100ms for subscriptions
428
+ - **Search**: Fuzzy matching with typo tolerance via pg_trgm
429
+
430
+ ## Security
431
+
432
+ - **SQL Injection Protection**: Parameterized queries only
433
+ - **RLS Enforcement**: Row-level security for all user data
434
+ - **Clerk JWT Verification**: Automatic user authentication
435
+ - **Tenant Isolation**: Multi-tenant data separation
436
+
437
+ ## Testing
438
+
439
+ This package uses testcontainers for real PostgreSQL testing (no mocks).
440
+
441
+ ```bash
442
+ npm test # Run tests in watch mode
443
+ npm run test:run # Run tests once
444
+ npm run test:coverage # Run with coverage report
445
+ ```
446
+
447
+ ## See Also
448
+
449
+ - [@bernierllc/database-adapter-core](../database-adapter-core) - Abstract base classes
450
+ - [@bernierllc/logger](../logger) - Structured logging
451
+ - [@bernierllc/retry-policy](../retry-policy) - Retry logic
452
+ - [@bernierllc/crypto-utils](../crypto-utils) - Cryptographic utilities
453
+
454
+ ## License
455
+
456
+ Copyright (c) 2025 Bernier LLC. All rights reserved.
@@ -0,0 +1,65 @@
1
+ import { SupabaseClient } from '@supabase/supabase-js';
2
+ import { BaseDatabaseAdapter, IDatabaseConfig, IDatabaseHealth, IQueryResult, ITransaction } from '@bernierllc/database-adapter-core';
3
+ /**
4
+ * Supabase-specific configuration
5
+ */
6
+ export interface ISupabaseConfig {
7
+ supabaseUrl?: string;
8
+ supabaseAnonKey?: string;
9
+ supabaseServiceKey?: string;
10
+ }
11
+ /**
12
+ * PostgreSQL database adapter with Supabase support
13
+ * Implements connection pooling, RLS, and real-time features
14
+ */
15
+ export declare class PostgreSQLAdapter extends BaseDatabaseAdapter {
16
+ private pool;
17
+ private supabase;
18
+ private rlsManager;
19
+ private logger;
20
+ constructor(config: IDatabaseConfig & ISupabaseConfig);
21
+ /**
22
+ * Connect to PostgreSQL with connection pooling
23
+ */
24
+ connect(): Promise<void>;
25
+ /**
26
+ * Disconnect and close pool
27
+ */
28
+ disconnect(): Promise<void>;
29
+ /**
30
+ * Execute query with RLS context
31
+ */
32
+ query<T = unknown>(sql: string, params?: unknown[]): Promise<IQueryResult<T>>;
33
+ /**
34
+ * Begin transaction with RLS context
35
+ */
36
+ beginTransaction(): Promise<ITransaction>;
37
+ /**
38
+ * Health check with connection pool stats
39
+ */
40
+ healthCheck(): Promise<IDatabaseHealth>;
41
+ /**
42
+ * Get Supabase client for real-time features
43
+ */
44
+ getSupabaseClient(): SupabaseClient | null;
45
+ /**
46
+ * Set RLS context for current operation
47
+ */
48
+ setRLSContext(userId: string, userRole: string, tenantId?: string): void;
49
+ /**
50
+ * Clear RLS context
51
+ */
52
+ clearRLSContext(): void;
53
+ /**
54
+ * Get current connection pool statistics
55
+ */
56
+ getPoolStats(): {
57
+ total: number;
58
+ idle: number;
59
+ waiting: number;
60
+ } | null;
61
+ /**
62
+ * Map PostgreSQL data type OID to name
63
+ */
64
+ private getDataTypeName;
65
+ }