singulio-postgres 1.1.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/dist/pool.js ADDED
@@ -0,0 +1,216 @@
1
+ /**
2
+ * @singulio/postgres - Connection Pool Manager
3
+ * Manages PostgreSQL connections with configurable pooling
4
+ *
5
+ * Note: Bun's native sql driver handles connection pooling internally.
6
+ * This module provides a higher-level API with explicit pool management
7
+ * for scenarios requiring fine-grained control.
8
+ */
9
+ import { sql } from 'bun';
10
+ /**
11
+ * Connection pool manager for PostgreSQL
12
+ */
13
+ export class PostgresPool {
14
+ config;
15
+ logger;
16
+ connections = new Map();
17
+ nextId = 0;
18
+ waitQueue = [];
19
+ closed = false;
20
+ idleCheckTimer;
21
+ constructor(config, logger) {
22
+ this.config = {
23
+ min: 2,
24
+ max: 20,
25
+ idleTimeout: 30000,
26
+ connectionTimeout: 10000,
27
+ maxLifetime: 3600000,
28
+ ...config,
29
+ };
30
+ this.logger = logger;
31
+ // Start idle connection checker
32
+ this.idleCheckTimer = setInterval(() => this.checkIdleConnections(), 10000);
33
+ }
34
+ /**
35
+ * Initialize the pool with minimum connections
36
+ */
37
+ async initialize() {
38
+ this.logger?.info('Initializing PostgreSQL pool', {
39
+ min: this.config.min,
40
+ max: this.config.max,
41
+ });
42
+ // Warm up minimum connections
43
+ for (let i = 0; i < this.config.min; i++) {
44
+ await this.createConnection();
45
+ }
46
+ this.logger?.info('PostgreSQL pool initialized', {
47
+ connections: this.connections.size,
48
+ });
49
+ }
50
+ async createConnection() {
51
+ const conn = {
52
+ id: this.nextId++,
53
+ createdAt: Date.now(),
54
+ lastUsedAt: Date.now(),
55
+ inUse: false,
56
+ };
57
+ this.connections.set(conn.id, conn);
58
+ return conn;
59
+ }
60
+ async acquireConnection() {
61
+ // Find an idle connection
62
+ for (const conn of this.connections.values()) {
63
+ if (!conn.inUse) {
64
+ // Check if connection is too old
65
+ if (Date.now() - conn.createdAt > this.config.maxLifetime) {
66
+ await this.destroyConnection(conn);
67
+ continue;
68
+ }
69
+ conn.inUse = true;
70
+ conn.lastUsedAt = Date.now();
71
+ return conn;
72
+ }
73
+ }
74
+ // Create new connection if under max
75
+ if (this.connections.size < this.config.max) {
76
+ const conn = await this.createConnection();
77
+ conn.inUse = true;
78
+ return conn;
79
+ }
80
+ // Wait for a connection
81
+ return new Promise((resolve, reject) => {
82
+ const timer = setTimeout(() => {
83
+ const idx = this.waitQueue.findIndex(w => w.resolve === resolve);
84
+ if (idx >= 0)
85
+ this.waitQueue.splice(idx, 1);
86
+ reject(new Error(`Connection timeout after ${this.config.connectionTimeout}ms`));
87
+ }, this.config.connectionTimeout);
88
+ this.waitQueue.push({ resolve, reject, timer });
89
+ });
90
+ }
91
+ releaseConnection(conn) {
92
+ conn.inUse = false;
93
+ conn.lastUsedAt = Date.now();
94
+ // Check waiting queue
95
+ if (this.waitQueue.length > 0) {
96
+ const waiter = this.waitQueue.shift();
97
+ clearTimeout(waiter.timer);
98
+ conn.inUse = true;
99
+ waiter.resolve(conn);
100
+ }
101
+ }
102
+ async destroyConnection(conn) {
103
+ this.connections.delete(conn.id);
104
+ this.logger?.debug('Connection destroyed', { id: conn.id });
105
+ }
106
+ async checkIdleConnections() {
107
+ if (this.closed)
108
+ return;
109
+ const now = Date.now();
110
+ for (const conn of this.connections.values()) {
111
+ // Don't close if we're at minimum
112
+ if (this.connections.size <= this.config.min)
113
+ break;
114
+ if (!conn.inUse && now - conn.lastUsedAt > this.config.idleTimeout) {
115
+ await this.destroyConnection(conn);
116
+ }
117
+ }
118
+ }
119
+ /**
120
+ * Execute a query using a pooled connection
121
+ */
122
+ async query(queryText, params) {
123
+ if (this.closed) {
124
+ throw new Error('Pool is closed');
125
+ }
126
+ const conn = await this.acquireConnection();
127
+ const start = performance.now();
128
+ try {
129
+ const result = await sql.unsafe(queryText, params ?? []);
130
+ const latency = performance.now() - start;
131
+ this.logger?.debug('Pool query executed', {
132
+ connId: conn.id,
133
+ query: queryText.substring(0, 100),
134
+ rows: result.length,
135
+ latencyMs: latency.toFixed(2),
136
+ });
137
+ return {
138
+ rows: result,
139
+ rowCount: result.length,
140
+ };
141
+ }
142
+ finally {
143
+ this.releaseConnection(conn);
144
+ }
145
+ }
146
+ /**
147
+ * Execute with exclusive connection for multi-statement operations
148
+ */
149
+ async withConnection(fn) {
150
+ if (this.closed) {
151
+ throw new Error('Pool is closed');
152
+ }
153
+ const conn = await this.acquireConnection();
154
+ try {
155
+ return await fn(async (queryText, params) => {
156
+ const result = await sql.unsafe(queryText, params ?? []);
157
+ return {
158
+ rows: result,
159
+ rowCount: result.length,
160
+ };
161
+ });
162
+ }
163
+ finally {
164
+ this.releaseConnection(conn);
165
+ }
166
+ }
167
+ /**
168
+ * Get current pool statistics
169
+ */
170
+ stats() {
171
+ let idle = 0;
172
+ let active = 0;
173
+ for (const conn of this.connections.values()) {
174
+ if (conn.inUse) {
175
+ active++;
176
+ }
177
+ else {
178
+ idle++;
179
+ }
180
+ }
181
+ return {
182
+ total: this.connections.size,
183
+ idle,
184
+ active,
185
+ pending: this.waitQueue.length,
186
+ };
187
+ }
188
+ /**
189
+ * Close the pool and all connections
190
+ */
191
+ async close() {
192
+ this.closed = true;
193
+ if (this.idleCheckTimer) {
194
+ clearInterval(this.idleCheckTimer);
195
+ }
196
+ // Reject all waiting
197
+ for (const waiter of this.waitQueue) {
198
+ clearTimeout(waiter.timer);
199
+ waiter.reject(new Error('Pool is closing'));
200
+ }
201
+ this.waitQueue = [];
202
+ // Destroy all connections
203
+ for (const conn of this.connections.values()) {
204
+ await this.destroyConnection(conn);
205
+ }
206
+ this.logger?.info('PostgreSQL pool closed');
207
+ }
208
+ }
209
+ /**
210
+ * Create and initialize a connection pool
211
+ */
212
+ export async function createPool(config, logger) {
213
+ const pool = new PostgresPool(config, logger);
214
+ await pool.initialize();
215
+ return pool;
216
+ }
package/dist/rls.d.ts ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @singulio/postgres - Row-Level Security Helpers
3
+ * Multi-tenant isolation via PostgreSQL session GUCs
4
+ */
5
+ import type { RLSContext, QueryResult, QueryRow, Logger } from './types.js';
6
+ /**
7
+ * Set RLS context variables for the current session
8
+ */
9
+ export declare function setRLSContext(context: RLSContext): Promise<void>;
10
+ /**
11
+ * Clear RLS context variables
12
+ */
13
+ export declare function clearRLSContext(): Promise<void>;
14
+ /**
15
+ * Get current RLS context
16
+ */
17
+ export declare function getRLSContext(): Promise<RLSContext | null>;
18
+ /**
19
+ * Execute a function with RLS context, automatically cleaning up afterward
20
+ */
21
+ export declare function withTenant<T>(context: RLSContext, fn: () => Promise<T>): Promise<T>;
22
+ /**
23
+ * Execute a query with RLS context
24
+ */
25
+ export declare function queryWithTenant<T = QueryRow>(context: RLSContext, queryText: string, params?: unknown[]): Promise<QueryResult<T>>;
26
+ /**
27
+ * RLS-aware client wrapper
28
+ */
29
+ export declare class RLSClient {
30
+ private logger?;
31
+ constructor(logger?: Logger);
32
+ /**
33
+ * Execute query with tenant context
34
+ */
35
+ query<T = QueryRow>(context: RLSContext, queryText: string, params?: unknown[]): Promise<QueryResult<T>>;
36
+ /**
37
+ * Execute with tenant context, return first row
38
+ */
39
+ queryOne<T = QueryRow>(context: RLSContext, queryText: string, params?: unknown[]): Promise<T | null>;
40
+ /**
41
+ * Execute with tenant context, return all rows
42
+ */
43
+ queryAll<T = QueryRow>(context: RLSContext, queryText: string, params?: unknown[]): Promise<T[]>;
44
+ /**
45
+ * Wrap a function with RLS context
46
+ */
47
+ withTenant<T>(context: RLSContext, fn: () => Promise<T>): Promise<T>;
48
+ }
49
+ /**
50
+ * Create an RLS-aware client
51
+ */
52
+ export declare function createRLSClient(logger?: Logger): RLSClient;
53
+ //# sourceMappingURL=rls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rls.d.ts","sourceRoot":"","sources":["../src/rls.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAE5E;;GAEG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBtE;AAED;;GAEG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAIrD;AAED;;GAEG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAgBhE;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAOzF;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,CAAC,GAAG,QAAQ,EAChD,OAAO,EAAE,UAAU,EACnB,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAQzB;AAED;;GAEG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAC,CAAS;gBAEZ,MAAM,CAAC,EAAE,MAAM;IAI3B;;OAEG;IACG,KAAK,CAAC,CAAC,GAAG,QAAQ,EACtB,OAAO,EAAE,UAAU,EACnB,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IAwB1B;;OAEG;IACG,QAAQ,CAAC,CAAC,GAAG,QAAQ,EACzB,OAAO,EAAE,UAAU,EACnB,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAKpB;;OAEG;IACG,QAAQ,CAAC,CAAC,GAAG,QAAQ,EACzB,OAAO,EAAE,UAAU,EACnB,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,CAAC,EAAE,CAAC;IAKf;;OAEG;IACH,UAAU,CAAC,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAGrE;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAE1D"}
package/dist/rls.js ADDED
@@ -0,0 +1,134 @@
1
+ /**
2
+ * @singulio/postgres - Row-Level Security Helpers
3
+ * Multi-tenant isolation via PostgreSQL session GUCs
4
+ */
5
+ import { sql } from 'bun';
6
+ /**
7
+ * Set RLS context variables for the current session
8
+ */
9
+ export async function setRLSContext(context) {
10
+ // Set tenant reference (required for RLS policies)
11
+ await sql `SELECT set_config('app.tenant_ref', ${context.tenantId}, TRUE)`;
12
+ // Set admin flag (bypasses some policies)
13
+ await sql `SELECT set_config('app.is_admin', ${context.isAdmin ? 't' : 'f'}, TRUE)`;
14
+ // Set user ID if provided
15
+ if (context.userId) {
16
+ await sql `SELECT set_config('app.user_id', ${context.userId}, TRUE)`;
17
+ }
18
+ // Set any extra GUCs
19
+ if (context.extraGUCs) {
20
+ for (const [key, value] of Object.entries(context.extraGUCs)) {
21
+ await sql.unsafe(`SELECT set_config($1, $2, TRUE)`, [key, value]);
22
+ }
23
+ }
24
+ }
25
+ /**
26
+ * Clear RLS context variables
27
+ */
28
+ export async function clearRLSContext() {
29
+ await sql `SELECT set_config('app.tenant_ref', '', TRUE)`;
30
+ await sql `SELECT set_config('app.is_admin', 'f', TRUE)`;
31
+ await sql `SELECT set_config('app.user_id', '', TRUE)`;
32
+ }
33
+ /**
34
+ * Get current RLS context
35
+ */
36
+ export async function getRLSContext() {
37
+ const result = await sql `
38
+ SELECT
39
+ current_setting('app.tenant_ref', TRUE) as tenant_id,
40
+ current_setting('app.user_id', TRUE) as user_id,
41
+ current_setting('app.is_admin', TRUE) as is_admin
42
+ `;
43
+ const row = result[0];
44
+ if (!row || !row.tenant_id)
45
+ return null;
46
+ return {
47
+ tenantId: row.tenant_id,
48
+ userId: row.user_id || undefined,
49
+ isAdmin: row.is_admin === 't',
50
+ };
51
+ }
52
+ /**
53
+ * Execute a function with RLS context, automatically cleaning up afterward
54
+ */
55
+ export async function withTenant(context, fn) {
56
+ await setRLSContext(context);
57
+ try {
58
+ return await fn();
59
+ }
60
+ finally {
61
+ await clearRLSContext();
62
+ }
63
+ }
64
+ /**
65
+ * Execute a query with RLS context
66
+ */
67
+ export async function queryWithTenant(context, queryText, params) {
68
+ return withTenant(context, async () => {
69
+ const result = await sql.unsafe(queryText, params ?? []);
70
+ return {
71
+ rows: result,
72
+ rowCount: result.length,
73
+ };
74
+ });
75
+ }
76
+ /**
77
+ * RLS-aware client wrapper
78
+ */
79
+ export class RLSClient {
80
+ logger;
81
+ constructor(logger) {
82
+ this.logger = logger;
83
+ }
84
+ /**
85
+ * Execute query with tenant context
86
+ */
87
+ async query(context, queryText, params) {
88
+ const start = performance.now();
89
+ try {
90
+ const result = await queryWithTenant(context, queryText, params);
91
+ this.logger?.debug('RLS query executed', {
92
+ tenant: context.tenantId,
93
+ query: queryText.substring(0, 100),
94
+ rows: result.rowCount,
95
+ latencyMs: (performance.now() - start).toFixed(2),
96
+ });
97
+ return result;
98
+ }
99
+ catch (error) {
100
+ this.logger?.error('RLS query failed', {
101
+ tenant: context.tenantId,
102
+ query: queryText.substring(0, 100),
103
+ error: error instanceof Error ? error.message : String(error),
104
+ });
105
+ throw error;
106
+ }
107
+ }
108
+ /**
109
+ * Execute with tenant context, return first row
110
+ */
111
+ async queryOne(context, queryText, params) {
112
+ const result = await this.query(context, queryText, params);
113
+ return result.rows[0] ?? null;
114
+ }
115
+ /**
116
+ * Execute with tenant context, return all rows
117
+ */
118
+ async queryAll(context, queryText, params) {
119
+ const result = await this.query(context, queryText, params);
120
+ return result.rows;
121
+ }
122
+ /**
123
+ * Wrap a function with RLS context
124
+ */
125
+ withTenant(context, fn) {
126
+ return withTenant(context, fn);
127
+ }
128
+ }
129
+ /**
130
+ * Create an RLS-aware client
131
+ */
132
+ export function createRLSClient(logger) {
133
+ return new RLSClient(logger);
134
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @singulio/postgres - Transaction Helpers
3
+ * ACID transaction support with auto-rollback
4
+ */
5
+ import type { TransactionOptions, RLSContext, QueryResult, QueryRow, Logger } from './types.js';
6
+ /**
7
+ * Transaction query interface
8
+ */
9
+ export interface TransactionQuery {
10
+ /**
11
+ * Execute a query within the transaction
12
+ */
13
+ query<T = QueryRow>(queryText: string, params?: unknown[]): Promise<QueryResult<T>>;
14
+ /**
15
+ * Execute and return first row
16
+ */
17
+ queryOne<T = QueryRow>(queryText: string, params?: unknown[]): Promise<T | null>;
18
+ /**
19
+ * Execute and return all rows
20
+ */
21
+ queryAll<T = QueryRow>(queryText: string, params?: unknown[]): Promise<T[]>;
22
+ /**
23
+ * Execute a command (INSERT/UPDATE/DELETE)
24
+ */
25
+ execute(queryText: string, params?: unknown[]): Promise<number>;
26
+ }
27
+ /**
28
+ * Execute a function within a transaction
29
+ * Auto-commits on success, auto-rollbacks on error
30
+ */
31
+ export declare function transaction<T>(fn: (tx: TransactionQuery) => Promise<T>, options?: TransactionOptions, logger?: Logger): Promise<T>;
32
+ /**
33
+ * Execute a function within a serializable transaction
34
+ * Best for operations requiring strong consistency
35
+ */
36
+ export declare function serializableTransaction<T>(fn: (tx: TransactionQuery) => Promise<T>, rlsContext?: RLSContext, logger?: Logger): Promise<T>;
37
+ /**
38
+ * Execute a function within a read-only transaction
39
+ * Best for reporting/analytics queries
40
+ */
41
+ export declare function readOnlyTransaction<T>(fn: (tx: TransactionQuery) => Promise<T>, rlsContext?: RLSContext, logger?: Logger): Promise<T>;
42
+ /**
43
+ * Create a savepoint within a transaction
44
+ */
45
+ export declare function savepoint(name: string): Promise<void>;
46
+ /**
47
+ * Rollback to a savepoint
48
+ */
49
+ export declare function rollbackToSavepoint(name: string): Promise<void>;
50
+ /**
51
+ * Release a savepoint
52
+ */
53
+ export declare function releaseSavepoint(name: string): Promise<void>;
54
+ //# sourceMappingURL=transaction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transaction.d.ts","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAwBhG;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;OAEG;IACH,KAAK,CAAC,CAAC,GAAG,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;IAEpF;;OAEG;IACH,QAAQ,CAAC,CAAC,GAAG,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAEjF;;OAEG;IACH,QAAQ,CAAC,CAAC,GAAG,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IAE5E;;OAEG;IACH,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACjE;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,CAAC,EACjC,EAAE,EAAE,CAAC,EAAE,EAAE,gBAAgB,KAAK,OAAO,CAAC,CAAC,CAAC,EACxC,OAAO,GAAE,kBAAuB,EAChC,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,CAAC,CAAC,CAoFZ;AAED;;;GAGG;AACH,wBAAsB,uBAAuB,CAAC,CAAC,EAC7C,EAAE,EAAE,CAAC,EAAE,EAAE,gBAAgB,KAAK,OAAO,CAAC,CAAC,CAAC,EACxC,UAAU,CAAC,EAAE,UAAU,EACvB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,CAAC,CAAC,CASZ;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,CAAC,EACzC,EAAE,EAAE,CAAC,EAAE,EAAE,gBAAgB,KAAK,OAAO,CAAC,CAAC,CAAC,EACxC,UAAU,CAAC,EAAE,UAAU,EACvB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,CAAC,CAAC,CASZ;AAED;;GAEG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE3D;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAErE;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAElE"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * @singulio/postgres - Transaction Helpers
3
+ * ACID transaction support with auto-rollback
4
+ */
5
+ import { sql } from 'bun';
6
+ import { setRLSContext, clearRLSContext } from './rls.js';
7
+ /**
8
+ * Build BEGIN statement with options
9
+ */
10
+ function buildBeginStatement(options) {
11
+ const parts = ['BEGIN'];
12
+ if (options.isolationLevel) {
13
+ parts.push(`ISOLATION LEVEL ${options.isolationLevel}`);
14
+ }
15
+ if (options.readOnly) {
16
+ parts.push('READ ONLY');
17
+ }
18
+ if (options.deferrable && options.readOnly && options.isolationLevel === 'SERIALIZABLE') {
19
+ parts.push('DEFERRABLE');
20
+ }
21
+ return parts.join(' ');
22
+ }
23
+ /**
24
+ * Execute a function within a transaction
25
+ * Auto-commits on success, auto-rollbacks on error
26
+ */
27
+ export async function transaction(fn, options = {}, logger) {
28
+ const start = performance.now();
29
+ // Begin transaction
30
+ const beginStmt = buildBeginStatement(options);
31
+ await sql.unsafe(beginStmt);
32
+ // Set RLS context if provided
33
+ if (options.rlsContext) {
34
+ await setRLSContext(options.rlsContext);
35
+ }
36
+ // Create query interface
37
+ const tx = {
38
+ async query(queryText, params) {
39
+ const result = await sql.unsafe(queryText, params ?? []);
40
+ return {
41
+ rows: result,
42
+ rowCount: result.length,
43
+ };
44
+ },
45
+ async queryOne(queryText, params) {
46
+ const result = await tx.query(queryText, params);
47
+ return result.rows[0] ?? null;
48
+ },
49
+ async queryAll(queryText, params) {
50
+ const result = await tx.query(queryText, params);
51
+ return result.rows;
52
+ },
53
+ async execute(queryText, params) {
54
+ const result = await tx.query(queryText, params);
55
+ return result.rowCount;
56
+ },
57
+ };
58
+ try {
59
+ const result = await fn(tx);
60
+ // Clear RLS context before commit
61
+ if (options.rlsContext) {
62
+ await clearRLSContext();
63
+ }
64
+ // Commit
65
+ await sql `COMMIT`;
66
+ const latency = performance.now() - start;
67
+ logger?.debug('Transaction committed', {
68
+ isolationLevel: options.isolationLevel ?? 'READ COMMITTED',
69
+ tenant: options.rlsContext?.tenantId,
70
+ latencyMs: latency.toFixed(2),
71
+ });
72
+ return result;
73
+ }
74
+ catch (error) {
75
+ // Clear RLS context before rollback
76
+ if (options.rlsContext) {
77
+ try {
78
+ await clearRLSContext();
79
+ }
80
+ catch {
81
+ // Ignore errors during cleanup
82
+ }
83
+ }
84
+ // Rollback
85
+ try {
86
+ await sql `ROLLBACK`;
87
+ }
88
+ catch {
89
+ // Connection might be broken
90
+ }
91
+ const latency = performance.now() - start;
92
+ logger?.error('Transaction rolled back', {
93
+ isolationLevel: options.isolationLevel ?? 'READ COMMITTED',
94
+ tenant: options.rlsContext?.tenantId,
95
+ error: error instanceof Error ? error.message : String(error),
96
+ latencyMs: latency.toFixed(2),
97
+ });
98
+ throw error;
99
+ }
100
+ }
101
+ /**
102
+ * Execute a function within a serializable transaction
103
+ * Best for operations requiring strong consistency
104
+ */
105
+ export async function serializableTransaction(fn, rlsContext, logger) {
106
+ return transaction(fn, {
107
+ isolationLevel: 'SERIALIZABLE',
108
+ rlsContext,
109
+ }, logger);
110
+ }
111
+ /**
112
+ * Execute a function within a read-only transaction
113
+ * Best for reporting/analytics queries
114
+ */
115
+ export async function readOnlyTransaction(fn, rlsContext, logger) {
116
+ return transaction(fn, {
117
+ readOnly: true,
118
+ rlsContext,
119
+ }, logger);
120
+ }
121
+ /**
122
+ * Create a savepoint within a transaction
123
+ */
124
+ export async function savepoint(name) {
125
+ await sql.unsafe(`SAVEPOINT ${name}`);
126
+ }
127
+ /**
128
+ * Rollback to a savepoint
129
+ */
130
+ export async function rollbackToSavepoint(name) {
131
+ await sql.unsafe(`ROLLBACK TO SAVEPOINT ${name}`);
132
+ }
133
+ /**
134
+ * Release a savepoint
135
+ */
136
+ export async function releaseSavepoint(name) {
137
+ await sql.unsafe(`RELEASE SAVEPOINT ${name}`);
138
+ }