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/README.md +866 -0
- package/dist/client.d.ts +52 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +127 -0
- package/dist/health.d.ts +44 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +182 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/pool.d.ts +52 -0
- package/dist/pool.d.ts.map +1 -0
- package/dist/pool.js +216 -0
- package/dist/rls.d.ts +53 -0
- package/dist/rls.d.ts.map +1 -0
- package/dist/rls.js +134 -0
- package/dist/transaction.d.ts +54 -0
- package/dist/transaction.d.ts.map +1 -0
- package/dist/transaction.js +138 -0
- package/dist/types.d.ts +121 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/vector.d.ts +74 -0
- package/dist/vector.d.ts.map +1 -0
- package/dist/vector.js +223 -0
- package/package.json +97 -0
- package/src/client.ts +153 -0
- package/src/health.ts +226 -0
- package/src/index.ts +110 -0
- package/src/pool.ts +268 -0
- package/src/rls.ts +169 -0
- package/src/transaction.ts +207 -0
- package/src/types.ts +142 -0
- package/src/vector.ts +312 -0
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
|
+
}
|