@geekmidas/db 0.0.4 → 0.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 +495 -0
- package/dist/kysely-0FOi6ZdO.d.cts +11 -0
- package/dist/kysely-8WPSKCZG.cjs +17 -0
- package/dist/kysely-8WPSKCZG.cjs.map +1 -0
- package/dist/kysely-Di1LVvL2.d.mts +11 -0
- package/dist/kysely-DmfA94RY.mjs +11 -0
- package/dist/kysely-DmfA94RY.mjs.map +1 -0
- package/dist/kysely.cjs +2 -8
- package/dist/kysely.d.cts +2 -7
- package/dist/kysely.d.mts +2 -7
- package/dist/kysely.mjs +1 -6
- package/dist/rls.cjs +72 -0
- package/dist/rls.cjs.map +1 -0
- package/dist/rls.d.cts +57 -0
- package/dist/rls.d.mts +57 -0
- package/dist/rls.mjs +48 -0
- package/dist/rls.mjs.map +1 -0
- package/package.json +13 -1
- package/src/__tests__/kysely.integration.spec.ts +741 -0
- package/src/__tests__/kysely.spec.ts +394 -0
- package/src/__tests__/rls.spec.ts +444 -0
- package/src/kysely.ts +18 -2
- package/src/rls.ts +90 -0
package/README.md
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
# @geekmidas/db
|
|
2
|
+
|
|
3
|
+
Database utilities for Kysely with flexible transaction management. Provides helpers for working with database connections and transactions in a type-safe way.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ **Flexible Transaction Handling**: Works with Kysely, Transaction, and ControlledTransaction
|
|
8
|
+
- ✅ **Automatic Transaction Detection**: Reuses existing transactions when nested
|
|
9
|
+
- ✅ **Type-Safe**: Full TypeScript support with generic database schemas
|
|
10
|
+
- ✅ **Connection Abstraction**: Single helper for all database connection types
|
|
11
|
+
- ✅ **Zero Dependencies**: Only peer dependency on Kysely
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add @geekmidas/db
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Peer Dependencies
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm add kysely pg
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { withTransaction } from '@geekmidas/db/kysely';
|
|
29
|
+
import type { DatabaseConnection } from '@geekmidas/db/kysely';
|
|
30
|
+
import { Kysely } from 'kysely';
|
|
31
|
+
|
|
32
|
+
interface Database {
|
|
33
|
+
users: {
|
|
34
|
+
id: string;
|
|
35
|
+
email: string;
|
|
36
|
+
name: string;
|
|
37
|
+
};
|
|
38
|
+
posts: {
|
|
39
|
+
id: string;
|
|
40
|
+
userId: string;
|
|
41
|
+
title: string;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function createUserWithPost(
|
|
46
|
+
db: DatabaseConnection<Database>,
|
|
47
|
+
userData: { email: string; name: string },
|
|
48
|
+
postData: { title: string }
|
|
49
|
+
) {
|
|
50
|
+
return withTransaction(db, async (trx) => {
|
|
51
|
+
// Create user
|
|
52
|
+
const user = await trx
|
|
53
|
+
.insertInto('users')
|
|
54
|
+
.values(userData)
|
|
55
|
+
.returningAll()
|
|
56
|
+
.executeTakeFirstOrThrow();
|
|
57
|
+
|
|
58
|
+
// Create post for user
|
|
59
|
+
const post = await trx
|
|
60
|
+
.insertInto('posts')
|
|
61
|
+
.values({
|
|
62
|
+
userId: user.id,
|
|
63
|
+
title: postData.title
|
|
64
|
+
})
|
|
65
|
+
.returningAll()
|
|
66
|
+
.executeTakeFirstOrThrow();
|
|
67
|
+
|
|
68
|
+
return { user, post };
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## API Reference
|
|
74
|
+
|
|
75
|
+
### `withTransaction`
|
|
76
|
+
|
|
77
|
+
Execute a callback within a transaction. If the connection is already a transaction, it reuses it. Otherwise, it creates a new transaction.
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
function withTransaction<DB, T>(
|
|
81
|
+
db: DatabaseConnection<DB>,
|
|
82
|
+
cb: (trx: Transaction<DB>) => Promise<T>
|
|
83
|
+
): Promise<T>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Parameters:**
|
|
87
|
+
- `db` - A database connection (Kysely, Transaction, or ControlledTransaction)
|
|
88
|
+
- `cb` - Callback function that receives the transaction
|
|
89
|
+
|
|
90
|
+
**Returns:**
|
|
91
|
+
- Promise resolving to the callback's return value
|
|
92
|
+
|
|
93
|
+
### `DatabaseConnection<T>`
|
|
94
|
+
|
|
95
|
+
Type union for all supported database connection types:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
type DatabaseConnection<T> =
|
|
99
|
+
| Kysely<T>
|
|
100
|
+
| Transaction<T>
|
|
101
|
+
| ControlledTransaction<T>;
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Usage Examples
|
|
105
|
+
|
|
106
|
+
### Basic Transaction
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { withTransaction } from '@geekmidas/db/kysely';
|
|
110
|
+
import { Kysely } from 'kysely';
|
|
111
|
+
|
|
112
|
+
const db = new Kysely<Database>({ /* config */ });
|
|
113
|
+
|
|
114
|
+
async function transferFunds(fromId: string, toId: string, amount: number) {
|
|
115
|
+
return withTransaction(db, async (trx) => {
|
|
116
|
+
// Deduct from sender
|
|
117
|
+
await trx
|
|
118
|
+
.updateTable('accounts')
|
|
119
|
+
.set({ balance: sql`balance - ${amount}` })
|
|
120
|
+
.where('id', '=', fromId)
|
|
121
|
+
.execute();
|
|
122
|
+
|
|
123
|
+
// Add to receiver
|
|
124
|
+
await trx
|
|
125
|
+
.updateTable('accounts')
|
|
126
|
+
.set({ balance: sql`balance + ${amount}` })
|
|
127
|
+
.where('id', '=', toId)
|
|
128
|
+
.execute();
|
|
129
|
+
|
|
130
|
+
return { success: true };
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Nested Transactions
|
|
136
|
+
|
|
137
|
+
The helper automatically detects existing transactions and reuses them:
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
async function createUser(
|
|
141
|
+
db: DatabaseConnection<Database>,
|
|
142
|
+
email: string
|
|
143
|
+
) {
|
|
144
|
+
return withTransaction(db, async (trx) => {
|
|
145
|
+
const user = await trx
|
|
146
|
+
.insertInto('users')
|
|
147
|
+
.values({ email })
|
|
148
|
+
.returningAll()
|
|
149
|
+
.executeTakeFirstOrThrow();
|
|
150
|
+
|
|
151
|
+
// This will reuse the same transaction
|
|
152
|
+
await createAuditLog(trx, 'user_created', user.id);
|
|
153
|
+
|
|
154
|
+
return user;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function createAuditLog(
|
|
159
|
+
db: DatabaseConnection<Database>,
|
|
160
|
+
action: string,
|
|
161
|
+
userId: string
|
|
162
|
+
) {
|
|
163
|
+
// If db is already a transaction, it's reused
|
|
164
|
+
return withTransaction(db, async (trx) => {
|
|
165
|
+
await trx
|
|
166
|
+
.insertInto('audit_logs')
|
|
167
|
+
.values({ action, userId, timestamp: new Date() })
|
|
168
|
+
.execute();
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Repository Pattern
|
|
174
|
+
|
|
175
|
+
Use `DatabaseConnection` type in repositories for flexibility:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import type { DatabaseConnection } from '@geekmidas/db/kysely';
|
|
179
|
+
import { withTransaction } from '@geekmidas/db/kysely';
|
|
180
|
+
|
|
181
|
+
class UserRepository {
|
|
182
|
+
constructor(private db: DatabaseConnection<Database>) {}
|
|
183
|
+
|
|
184
|
+
async create(data: NewUser): Promise<User> {
|
|
185
|
+
return withTransaction(this.db, async (trx) => {
|
|
186
|
+
return trx
|
|
187
|
+
.insertInto('users')
|
|
188
|
+
.values(data)
|
|
189
|
+
.returningAll()
|
|
190
|
+
.executeTakeFirstOrThrow();
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async update(id: string, data: UserUpdate): Promise<User> {
|
|
195
|
+
return withTransaction(this.db, async (trx) => {
|
|
196
|
+
return trx
|
|
197
|
+
.updateTable('users')
|
|
198
|
+
.set(data)
|
|
199
|
+
.where('id', '=', id)
|
|
200
|
+
.returningAll()
|
|
201
|
+
.executeTakeFirstOrThrow();
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Can be used with any connection type
|
|
207
|
+
const db = new Kysely<Database>({ /* config */ });
|
|
208
|
+
const repo = new UserRepository(db);
|
|
209
|
+
|
|
210
|
+
// Or within a transaction
|
|
211
|
+
await withTransaction(db, async (trx) => {
|
|
212
|
+
const repo = new UserRepository(trx);
|
|
213
|
+
await repo.create({ email: 'user@example.com' });
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Service Pattern with Transactions
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
import type { DatabaseConnection } from '@geekmidas/db/kysely';
|
|
221
|
+
import { withTransaction } from '@geekmidas/db/kysely';
|
|
222
|
+
|
|
223
|
+
class OrderService {
|
|
224
|
+
constructor(
|
|
225
|
+
private db: DatabaseConnection<Database>,
|
|
226
|
+
private inventoryService: InventoryService,
|
|
227
|
+
private paymentService: PaymentService
|
|
228
|
+
) {}
|
|
229
|
+
|
|
230
|
+
async createOrder(
|
|
231
|
+
userId: string,
|
|
232
|
+
items: OrderItem[]
|
|
233
|
+
): Promise<Order> {
|
|
234
|
+
return withTransaction(this.db, async (trx) => {
|
|
235
|
+
// All operations share the same transaction
|
|
236
|
+
const order = await trx
|
|
237
|
+
.insertInto('orders')
|
|
238
|
+
.values({ userId, status: 'pending' })
|
|
239
|
+
.returningAll()
|
|
240
|
+
.executeTakeFirstOrThrow();
|
|
241
|
+
|
|
242
|
+
// These services can accept the transaction
|
|
243
|
+
await this.inventoryService.reserveItems(trx, items);
|
|
244
|
+
await this.paymentService.processPayment(trx, order.id);
|
|
245
|
+
|
|
246
|
+
// Update order status
|
|
247
|
+
return trx
|
|
248
|
+
.updateTable('orders')
|
|
249
|
+
.set({ status: 'completed' })
|
|
250
|
+
.where('id', '=', order.id)
|
|
251
|
+
.returningAll()
|
|
252
|
+
.executeTakeFirstOrThrow();
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
class InventoryService {
|
|
258
|
+
async reserveItems(
|
|
259
|
+
db: DatabaseConnection<Database>,
|
|
260
|
+
items: OrderItem[]
|
|
261
|
+
) {
|
|
262
|
+
return withTransaction(db, async (trx) => {
|
|
263
|
+
for (const item of items) {
|
|
264
|
+
await trx
|
|
265
|
+
.updateTable('inventory')
|
|
266
|
+
.set({ reserved: sql`reserved + ${item.quantity}` })
|
|
267
|
+
.where('productId', '=', item.productId)
|
|
268
|
+
.execute();
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Error Handling
|
|
276
|
+
|
|
277
|
+
Transactions automatically roll back on errors:
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
import { withTransaction } from '@geekmidas/db/kysely';
|
|
281
|
+
|
|
282
|
+
async function processOrder(db: DatabaseConnection<Database>, orderId: string) {
|
|
283
|
+
try {
|
|
284
|
+
return await withTransaction(db, async (trx) => {
|
|
285
|
+
const order = await trx
|
|
286
|
+
.selectFrom('orders')
|
|
287
|
+
.where('id', '=', orderId)
|
|
288
|
+
.selectAll()
|
|
289
|
+
.executeTakeFirstOrThrow();
|
|
290
|
+
|
|
291
|
+
if (order.status !== 'pending') {
|
|
292
|
+
throw new Error('Order already processed');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Update order
|
|
296
|
+
await trx
|
|
297
|
+
.updateTable('orders')
|
|
298
|
+
.set({ status: 'processing' })
|
|
299
|
+
.where('id', '=', orderId)
|
|
300
|
+
.execute();
|
|
301
|
+
|
|
302
|
+
// If this throws, the entire transaction rolls back
|
|
303
|
+
await processPayment(trx, orderId);
|
|
304
|
+
|
|
305
|
+
return order;
|
|
306
|
+
});
|
|
307
|
+
} catch (error) {
|
|
308
|
+
console.error('Transaction failed:', error);
|
|
309
|
+
// Transaction has been rolled back
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Testing with Transactions
|
|
316
|
+
|
|
317
|
+
Use transactions for test isolation:
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
import { describe, it, beforeEach, afterEach } from 'vitest';
|
|
321
|
+
import type { Transaction } from 'kysely';
|
|
322
|
+
|
|
323
|
+
describe('UserRepository', () => {
|
|
324
|
+
let trx: Transaction<Database>;
|
|
325
|
+
|
|
326
|
+
beforeEach(async () => {
|
|
327
|
+
trx = await db.transaction().execute(async (t) => t);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
afterEach(async () => {
|
|
331
|
+
await trx.rollback();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should create user', async () => {
|
|
335
|
+
const repo = new UserRepository(trx);
|
|
336
|
+
const user = await repo.create({ email: 'test@example.com' });
|
|
337
|
+
|
|
338
|
+
expect(user.email).toBe('test@example.com');
|
|
339
|
+
// Transaction will be rolled back after test
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
## Type Safety
|
|
345
|
+
|
|
346
|
+
The package provides full type safety for database operations:
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
import type { DatabaseConnection } from '@geekmidas/db/kysely';
|
|
350
|
+
import { withTransaction } from '@geekmidas/db/kysely';
|
|
351
|
+
|
|
352
|
+
interface Database {
|
|
353
|
+
users: {
|
|
354
|
+
id: Generated<string>;
|
|
355
|
+
email: string;
|
|
356
|
+
name: string | null;
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function updateUser(
|
|
361
|
+
db: DatabaseConnection<Database>,
|
|
362
|
+
id: string,
|
|
363
|
+
data: { name: string }
|
|
364
|
+
) {
|
|
365
|
+
return withTransaction(db, async (trx) => {
|
|
366
|
+
// Full autocomplete and type checking
|
|
367
|
+
return trx
|
|
368
|
+
.updateTable('users')
|
|
369
|
+
.set(data) // Type-checked against users table
|
|
370
|
+
.where('id', '=', id)
|
|
371
|
+
.returningAll()
|
|
372
|
+
.executeTakeFirstOrThrow();
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## Advanced Patterns
|
|
378
|
+
|
|
379
|
+
### Unit of Work Pattern
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
class UnitOfWork {
|
|
383
|
+
private transaction: Transaction<Database> | null = null;
|
|
384
|
+
|
|
385
|
+
constructor(private db: Kysely<Database>) {}
|
|
386
|
+
|
|
387
|
+
async begin() {
|
|
388
|
+
this.transaction = await this.db.transaction().execute(async (t) => t);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async commit() {
|
|
392
|
+
if (this.transaction) {
|
|
393
|
+
await this.transaction.commit();
|
|
394
|
+
this.transaction = null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async rollback() {
|
|
399
|
+
if (this.transaction) {
|
|
400
|
+
await this.transaction.rollback();
|
|
401
|
+
this.transaction = null;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
getConnection(): DatabaseConnection<Database> {
|
|
406
|
+
return this.transaction || this.db;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Usage
|
|
411
|
+
const uow = new UnitOfWork(db);
|
|
412
|
+
await uow.begin();
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
const userRepo = new UserRepository(uow.getConnection());
|
|
416
|
+
const orderRepo = new OrderRepository(uow.getConnection());
|
|
417
|
+
|
|
418
|
+
await userRepo.create({ email: 'user@example.com' });
|
|
419
|
+
await orderRepo.create({ userId: '123' });
|
|
420
|
+
|
|
421
|
+
await uow.commit();
|
|
422
|
+
} catch (error) {
|
|
423
|
+
await uow.rollback();
|
|
424
|
+
throw error;
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### Connection Pooling
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
import { Pool } from 'pg';
|
|
432
|
+
import { Kysely, PostgresDialect } from 'kysely';
|
|
433
|
+
|
|
434
|
+
const pool = new Pool({
|
|
435
|
+
host: 'localhost',
|
|
436
|
+
database: 'mydb',
|
|
437
|
+
max: 10
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
const db = new Kysely<Database>({
|
|
441
|
+
dialect: new PostgresDialect({ pool })
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// Use with withTransaction
|
|
445
|
+
async function processData(data: unknown[]) {
|
|
446
|
+
return withTransaction(db, async (trx) => {
|
|
447
|
+
// Each transaction gets a connection from the pool
|
|
448
|
+
for (const item of data) {
|
|
449
|
+
await trx.insertInto('items').values(item).execute();
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
## Best Practices
|
|
456
|
+
|
|
457
|
+
1. **Use DatabaseConnection Type**: Accept `DatabaseConnection` in functions that work with transactions
|
|
458
|
+
```typescript
|
|
459
|
+
async function myFunction(db: DatabaseConnection<Database>) { }
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
2. **Let withTransaction Handle Reuse**: Don't manually check for transaction type
|
|
463
|
+
```typescript
|
|
464
|
+
// Good
|
|
465
|
+
await withTransaction(db, async (trx) => { });
|
|
466
|
+
|
|
467
|
+
// Avoid
|
|
468
|
+
if (db.isTransaction) { /* ... */ } else { /* ... */ }
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
3. **Keep Transactions Short**: Execute quickly to avoid blocking
|
|
472
|
+
```typescript
|
|
473
|
+
// Good
|
|
474
|
+
await withTransaction(db, async (trx) => {
|
|
475
|
+
await trx.insertInto('users').values(data).execute();
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// Avoid long-running operations
|
|
479
|
+
await withTransaction(db, async (trx) => {
|
|
480
|
+
await fetch('https://api.example.com'); // Bad!
|
|
481
|
+
});
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
4. **Error Handling**: Let transactions roll back automatically on errors
|
|
485
|
+
|
|
486
|
+
5. **Testing**: Use transactions for test isolation with automatic rollback
|
|
487
|
+
|
|
488
|
+
## Related Packages
|
|
489
|
+
|
|
490
|
+
- [Kysely](https://github.com/kysely-org/kysely) - Type-safe SQL query builder
|
|
491
|
+
- [@geekmidas/testkit](../testkit) - Testing utilities with database factories
|
|
492
|
+
|
|
493
|
+
## License
|
|
494
|
+
|
|
495
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ControlledTransaction, IsolationLevel, Kysely, Transaction } from "kysely";
|
|
2
|
+
|
|
3
|
+
//#region src/kysely.d.ts
|
|
4
|
+
interface TransactionSettings {
|
|
5
|
+
isolationLevel?: IsolationLevel;
|
|
6
|
+
}
|
|
7
|
+
declare function withTransaction<DB, T>(db: DatabaseConnection<DB>, cb: (trx: Transaction<DB>) => Promise<T>, settings?: TransactionSettings): Promise<T>;
|
|
8
|
+
type DatabaseConnection<T> = ControlledTransaction<T> | Kysely<T> | Transaction<T>;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { DatabaseConnection, TransactionSettings, withTransaction };
|
|
11
|
+
//# sourceMappingURL=kysely-0FOi6ZdO.d.cts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/kysely.ts
|
|
3
|
+
function withTransaction(db, cb, settings) {
|
|
4
|
+
if (db.isTransaction) return cb(db);
|
|
5
|
+
const builder = db.transaction();
|
|
6
|
+
if (settings?.isolationLevel) return builder.setIsolationLevel(settings.isolationLevel).execute(cb);
|
|
7
|
+
return builder.execute(cb);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
//#endregion
|
|
11
|
+
Object.defineProperty(exports, 'withTransaction', {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
get: function () {
|
|
14
|
+
return withTransaction;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
//# sourceMappingURL=kysely-8WPSKCZG.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kysely-8WPSKCZG.cjs","names":["db: DatabaseConnection<DB>","cb: (trx: Transaction<DB>) => Promise<T>","settings?: TransactionSettings"],"sources":["../src/kysely.ts"],"sourcesContent":["import type {\n ControlledTransaction,\n IsolationLevel,\n Kysely,\n Transaction,\n} from 'kysely';\n\nexport interface TransactionSettings {\n isolationLevel?: IsolationLevel;\n}\n\nexport function withTransaction<DB, T>(\n db: DatabaseConnection<DB>,\n cb: (trx: Transaction<DB>) => Promise<T>,\n settings?: TransactionSettings,\n): Promise<T> {\n if (db.isTransaction) {\n return cb(db as Transaction<DB>);\n }\n\n const builder = db.transaction();\n\n if (settings?.isolationLevel) {\n return builder.setIsolationLevel(settings.isolationLevel).execute(cb);\n }\n\n return builder.execute(cb);\n}\n\nexport type DatabaseConnection<T> =\n | ControlledTransaction<T>\n | Kysely<T>\n | Transaction<T>;\n"],"mappings":";;AAWA,SAAgB,gBACdA,IACAC,IACAC,UACY;AACZ,KAAI,GAAG,cACL,QAAO,GAAG,GAAsB;CAGlC,MAAM,UAAU,GAAG,aAAa;AAEhC,KAAI,UAAU,eACZ,QAAO,QAAQ,kBAAkB,SAAS,eAAe,CAAC,QAAQ,GAAG;AAGvE,QAAO,QAAQ,QAAQ,GAAG;AAC3B"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ControlledTransaction, IsolationLevel, Kysely, Transaction } from "kysely";
|
|
2
|
+
|
|
3
|
+
//#region src/kysely.d.ts
|
|
4
|
+
interface TransactionSettings {
|
|
5
|
+
isolationLevel?: IsolationLevel;
|
|
6
|
+
}
|
|
7
|
+
declare function withTransaction<DB, T>(db: DatabaseConnection<DB>, cb: (trx: Transaction<DB>) => Promise<T>, settings?: TransactionSettings): Promise<T>;
|
|
8
|
+
type DatabaseConnection<T> = ControlledTransaction<T> | Kysely<T> | Transaction<T>;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { DatabaseConnection, TransactionSettings, withTransaction };
|
|
11
|
+
//# sourceMappingURL=kysely-Di1LVvL2.d.mts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/kysely.ts
|
|
2
|
+
function withTransaction(db, cb, settings) {
|
|
3
|
+
if (db.isTransaction) return cb(db);
|
|
4
|
+
const builder = db.transaction();
|
|
5
|
+
if (settings?.isolationLevel) return builder.setIsolationLevel(settings.isolationLevel).execute(cb);
|
|
6
|
+
return builder.execute(cb);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
//#endregion
|
|
10
|
+
export { withTransaction };
|
|
11
|
+
//# sourceMappingURL=kysely-DmfA94RY.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kysely-DmfA94RY.mjs","names":["db: DatabaseConnection<DB>","cb: (trx: Transaction<DB>) => Promise<T>","settings?: TransactionSettings"],"sources":["../src/kysely.ts"],"sourcesContent":["import type {\n ControlledTransaction,\n IsolationLevel,\n Kysely,\n Transaction,\n} from 'kysely';\n\nexport interface TransactionSettings {\n isolationLevel?: IsolationLevel;\n}\n\nexport function withTransaction<DB, T>(\n db: DatabaseConnection<DB>,\n cb: (trx: Transaction<DB>) => Promise<T>,\n settings?: TransactionSettings,\n): Promise<T> {\n if (db.isTransaction) {\n return cb(db as Transaction<DB>);\n }\n\n const builder = db.transaction();\n\n if (settings?.isolationLevel) {\n return builder.setIsolationLevel(settings.isolationLevel).execute(cb);\n }\n\n return builder.execute(cb);\n}\n\nexport type DatabaseConnection<T> =\n | ControlledTransaction<T>\n | Kysely<T>\n | Transaction<T>;\n"],"mappings":";AAWA,SAAgB,gBACdA,IACAC,IACAC,UACY;AACZ,KAAI,GAAG,cACL,QAAO,GAAG,GAAsB;CAGlC,MAAM,UAAU,GAAG,aAAa;AAEhC,KAAI,UAAU,eACZ,QAAO,QAAQ,kBAAkB,SAAS,eAAe,CAAC,QAAQ,GAAG;AAGvE,QAAO,QAAQ,QAAQ,GAAG;AAC3B"}
|
package/dist/kysely.cjs
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
+
const require_kysely = require('./kysely-8WPSKCZG.cjs');
|
|
1
2
|
|
|
2
|
-
|
|
3
|
-
function withTransaction(db, cb) {
|
|
4
|
-
if (db.isTransaction) return cb(db);
|
|
5
|
-
return db.transaction().execute(cb);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
//#endregion
|
|
9
|
-
exports.withTransaction = withTransaction;
|
|
3
|
+
exports.withTransaction = require_kysely.withTransaction;
|
package/dist/kysely.d.cts
CHANGED
|
@@ -1,7 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
//#region src/kysely.d.ts
|
|
4
|
-
declare function withTransaction<DB, T>(db: DatabaseConnection<DB>, cb: (trx: Transaction<DB>) => Promise<T>): Promise<T>;
|
|
5
|
-
type DatabaseConnection<T> = ControlledTransaction<T> | Kysely<T> | Transaction<T>;
|
|
6
|
-
//#endregion
|
|
7
|
-
export { DatabaseConnection, withTransaction };
|
|
1
|
+
import { DatabaseConnection, TransactionSettings, withTransaction } from "./kysely-0FOi6ZdO.cjs";
|
|
2
|
+
export { DatabaseConnection, TransactionSettings, withTransaction };
|
package/dist/kysely.d.mts
CHANGED
|
@@ -1,7 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
//#region src/kysely.d.ts
|
|
4
|
-
declare function withTransaction<DB, T>(db: DatabaseConnection<DB>, cb: (trx: Transaction<DB>) => Promise<T>): Promise<T>;
|
|
5
|
-
type DatabaseConnection<T> = ControlledTransaction<T> | Kysely<T> | Transaction<T>;
|
|
6
|
-
//#endregion
|
|
7
|
-
export { DatabaseConnection, withTransaction };
|
|
1
|
+
import { DatabaseConnection, TransactionSettings, withTransaction } from "./kysely-Di1LVvL2.mjs";
|
|
2
|
+
export { DatabaseConnection, TransactionSettings, withTransaction };
|
package/dist/kysely.mjs
CHANGED
package/dist/rls.cjs
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
//#region rolldown:runtime
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
const require_kysely = require('./kysely-8WPSKCZG.cjs');
|
|
25
|
+
const kysely = __toESM(require("kysely"));
|
|
26
|
+
|
|
27
|
+
//#region src/rls.ts
|
|
28
|
+
/**
|
|
29
|
+
* Execute a callback within a transaction with RLS context variables set.
|
|
30
|
+
*
|
|
31
|
+
* Sets PostgreSQL session variables using `SET LOCAL` which scopes them to the
|
|
32
|
+
* current transaction. Variables are automatically cleared when the transaction
|
|
33
|
+
* ends (commit or rollback).
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* await withRlsContext(
|
|
38
|
+
* db,
|
|
39
|
+
* { user_id: session.userId, tenant_id: session.tenantId },
|
|
40
|
+
* async (trx) => {
|
|
41
|
+
* // RLS policies can now use current_setting('app.user_id')
|
|
42
|
+
* return trx.selectFrom('orders').selectAll().execute();
|
|
43
|
+
* }
|
|
44
|
+
* );
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @param db - Database connection (Kysely, Transaction, or ControlledTransaction)
|
|
48
|
+
* @param context - Key-value pairs to set as session variables
|
|
49
|
+
* @param callback - Function to execute within the RLS context
|
|
50
|
+
* @param options - Optional prefix and transaction settings
|
|
51
|
+
*/
|
|
52
|
+
async function withRlsContext(db, context, callback, options) {
|
|
53
|
+
const prefix = options?.prefix ?? "app";
|
|
54
|
+
return require_kysely.withTransaction(db, async (trx) => {
|
|
55
|
+
for (const [key, value] of Object.entries(context)) {
|
|
56
|
+
if (value === null || value === void 0) continue;
|
|
57
|
+
const settingName = `${prefix}.${key}`;
|
|
58
|
+
const settingValue = String(value);
|
|
59
|
+
await kysely.sql`SELECT set_config(${settingName}, ${settingValue}, true)`.execute(trx);
|
|
60
|
+
}
|
|
61
|
+
return callback(trx);
|
|
62
|
+
}, options?.settings);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Bypass marker symbol for explicitly skipping RLS context.
|
|
66
|
+
*/
|
|
67
|
+
const RLS_BYPASS = Symbol.for("geekmidas.rls.bypass");
|
|
68
|
+
|
|
69
|
+
//#endregion
|
|
70
|
+
exports.RLS_BYPASS = RLS_BYPASS;
|
|
71
|
+
exports.withRlsContext = withRlsContext;
|
|
72
|
+
//# sourceMappingURL=rls.cjs.map
|
package/dist/rls.cjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rls.cjs","names":["db: DatabaseConnection<DB>","context: RlsContext","callback: (trx: Transaction<DB>) => Promise<T>","options?: WithRlsContextOptions"],"sources":["../src/rls.ts"],"sourcesContent":["import type { Transaction } from 'kysely';\nimport { sql } from 'kysely';\nimport {\n type DatabaseConnection,\n type TransactionSettings,\n withTransaction,\n} from './kysely';\n\n/**\n * RLS context - key-value pairs to set as PostgreSQL session variables.\n * Keys become `prefix.key` (e.g., `app.user_id`).\n */\nexport interface RlsContext {\n [key: string]: string | number | boolean | null | undefined;\n}\n\n/**\n * Options for withRlsContext function.\n */\nexport interface WithRlsContextOptions {\n /** Prefix for PostgreSQL session variables (default: 'app') */\n prefix?: string;\n /** Transaction settings (isolation level) */\n settings?: TransactionSettings;\n}\n\n/**\n * Execute a callback within a transaction with RLS context variables set.\n *\n * Sets PostgreSQL session variables using `SET LOCAL` which scopes them to the\n * current transaction. Variables are automatically cleared when the transaction\n * ends (commit or rollback).\n *\n * @example\n * ```ts\n * await withRlsContext(\n * db,\n * { user_id: session.userId, tenant_id: session.tenantId },\n * async (trx) => {\n * // RLS policies can now use current_setting('app.user_id')\n * return trx.selectFrom('orders').selectAll().execute();\n * }\n * );\n * ```\n *\n * @param db - Database connection (Kysely, Transaction, or ControlledTransaction)\n * @param context - Key-value pairs to set as session variables\n * @param callback - Function to execute within the RLS context\n * @param options - Optional prefix and transaction settings\n */\nexport async function withRlsContext<DB, T>(\n db: DatabaseConnection<DB>,\n context: RlsContext,\n callback: (trx: Transaction<DB>) => Promise<T>,\n options?: WithRlsContextOptions,\n): Promise<T> {\n const prefix = options?.prefix ?? 'app';\n\n return withTransaction(\n db,\n async (trx) => {\n // Set each context variable using SET LOCAL (scoped to transaction)\n for (const [key, value] of Object.entries(context)) {\n if (value === null || value === undefined) continue;\n\n const settingName = `${prefix}.${key}`;\n const settingValue = String(value);\n\n // Use raw SQL for SET LOCAL with proper escaping\n // The setting name is an identifier, value is a string literal\n await sql`SELECT set_config(${settingName}, ${settingValue}, true)`.execute(\n trx,\n );\n }\n\n return callback(trx);\n },\n options?.settings,\n );\n}\n\n/**\n * Bypass marker symbol for explicitly skipping RLS context.\n */\nexport const RLS_BYPASS = Symbol.for('geekmidas.rls.bypass');\n\n/**\n * Type for RLS bypass marker.\n */\nexport type RlsBypass = typeof RLS_BYPASS;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,eAAsB,eACpBA,IACAC,SACAC,UACAC,SACY;CACZ,MAAM,SAAS,SAAS,UAAU;AAElC,QAAO,+BACL,IACA,OAAO,QAAQ;AAEb,OAAK,MAAM,CAAC,KAAK,MAAM,IAAI,OAAO,QAAQ,QAAQ,EAAE;AAClD,OAAI,UAAU,QAAQ,iBAAqB;GAE3C,MAAM,eAAe,EAAE,OAAO,GAAG,IAAI;GACrC,MAAM,eAAe,OAAO,MAAM;AAIlC,SAAM,WAAI,oBAAoB,YAAY,IAAI,aAAa,SAAS,QAClE,IACD;EACF;AAED,SAAO,SAAS,IAAI;CACrB,GACD,SAAS,SACV;AACF;;;;AAKD,MAAa,aAAa,OAAO,IAAI,uBAAuB"}
|