@geekmidas/db 0.1.0 → 0.3.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.
@@ -1,9 +1,9 @@
1
1
  import {
2
- CamelCasePlugin,
3
- type Generated,
4
- Kysely,
5
- PostgresDialect,
6
- sql,
2
+ CamelCasePlugin,
3
+ type Generated,
4
+ Kysely,
5
+ PostgresDialect,
6
+ sql,
7
7
  } from 'kysely';
8
8
  import pg from 'pg';
9
9
  import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
@@ -11,434 +11,434 @@ import { TEST_DATABASE_CONFIG } from '../../../testkit/test/globalSetup';
11
11
  import { RLS_BYPASS, type RlsContext, withRlsContext } from '../rls';
12
12
 
13
13
  interface TestDatabase {
14
- rlsTestOrders: {
15
- id: Generated<number>;
16
- tenantId: string;
17
- userId: string;
18
- amount: number;
19
- createdAt: Generated<Date>;
20
- };
14
+ rlsTestOrders: {
15
+ id: Generated<number>;
16
+ tenantId: string;
17
+ userId: string;
18
+ amount: number;
19
+ createdAt: Generated<Date>;
20
+ };
21
21
  }
22
22
 
23
23
  describe('RLS Utility - Integration Tests', () => {
24
- let db: Kysely<TestDatabase>;
25
-
26
- beforeAll(async () => {
27
- db = new Kysely<TestDatabase>({
28
- dialect: new PostgresDialect({
29
- pool: new pg.Pool({
30
- ...TEST_DATABASE_CONFIG,
31
- database: 'postgres',
32
- }),
33
- }),
34
- plugins: [new CamelCasePlugin()],
35
- });
36
-
37
- // Create test table
38
- await db.schema
39
- .createTable('rls_test_orders')
40
- .ifNotExists()
41
- .addColumn('id', 'serial', (col) => col.primaryKey())
42
- .addColumn('tenant_id', 'varchar', (col) => col.notNull())
43
- .addColumn('user_id', 'varchar', (col) => col.notNull())
44
- .addColumn('amount', 'numeric(10, 2)', (col) => col.notNull())
45
- .addColumn('created_at', 'timestamp', (col) =>
46
- col.defaultTo(sql`now()`).notNull(),
47
- )
48
- .execute();
49
- });
50
-
51
- afterEach(async () => {
52
- // Clean up data after each test
53
- await db.deleteFrom('rlsTestOrders').execute();
54
- });
55
-
56
- afterAll(async () => {
57
- // Drop table and close connection
58
- await db.schema.dropTable('rls_test_orders').ifExists().execute();
59
- await db.destroy();
60
- });
61
-
62
- describe('withRlsContext', () => {
63
- it('should execute callback within transaction', async () => {
64
- const result = await withRlsContext(
65
- db,
66
- { user_id: 'user-123' },
67
- async (trx) => {
68
- // Insert an order within the RLS context
69
- const order = await trx
70
- .insertInto('rlsTestOrders')
71
- .values({
72
- tenantId: 'tenant-1',
73
- userId: 'user-123',
74
- amount: 100,
75
- })
76
- .returningAll()
77
- .executeTakeFirstOrThrow();
78
-
79
- return order;
80
- },
81
- );
82
-
83
- expect(result.id).toBeDefined();
84
- expect(result.userId).toBe('user-123');
85
- });
86
-
87
- it('should set RLS context variables with set_config', async () => {
88
- const capturedValues = await withRlsContext(
89
- db,
90
- { user_id: 'user-123', tenant_id: 'tenant-456' },
91
- async (trx) => {
92
- // Read the session variables using current_setting
93
- const userId = await sql<{ value: string }>`
24
+ let db: Kysely<TestDatabase>;
25
+
26
+ beforeAll(async () => {
27
+ db = new Kysely<TestDatabase>({
28
+ dialect: new PostgresDialect({
29
+ pool: new pg.Pool({
30
+ ...TEST_DATABASE_CONFIG,
31
+ database: 'postgres',
32
+ }),
33
+ }),
34
+ plugins: [new CamelCasePlugin()],
35
+ });
36
+
37
+ // Create test table
38
+ await db.schema
39
+ .createTable('rls_test_orders')
40
+ .ifNotExists()
41
+ .addColumn('id', 'serial', (col) => col.primaryKey())
42
+ .addColumn('tenant_id', 'varchar', (col) => col.notNull())
43
+ .addColumn('user_id', 'varchar', (col) => col.notNull())
44
+ .addColumn('amount', 'numeric(10, 2)', (col) => col.notNull())
45
+ .addColumn('created_at', 'timestamp', (col) =>
46
+ col.defaultTo(sql`now()`).notNull(),
47
+ )
48
+ .execute();
49
+ });
50
+
51
+ afterEach(async () => {
52
+ // Clean up data after each test
53
+ await db.deleteFrom('rlsTestOrders').execute();
54
+ });
55
+
56
+ afterAll(async () => {
57
+ // Drop table and close connection
58
+ await db.schema.dropTable('rls_test_orders').ifExists().execute();
59
+ await db.destroy();
60
+ });
61
+
62
+ describe('withRlsContext', () => {
63
+ it('should execute callback within transaction', async () => {
64
+ const result = await withRlsContext(
65
+ db,
66
+ { user_id: 'user-123' },
67
+ async (trx) => {
68
+ // Insert an order within the RLS context
69
+ const order = await trx
70
+ .insertInto('rlsTestOrders')
71
+ .values({
72
+ tenantId: 'tenant-1',
73
+ userId: 'user-123',
74
+ amount: 100,
75
+ })
76
+ .returningAll()
77
+ .executeTakeFirstOrThrow();
78
+
79
+ return order;
80
+ },
81
+ );
82
+
83
+ expect(result.id).toBeDefined();
84
+ expect(result.userId).toBe('user-123');
85
+ });
86
+
87
+ it('should set RLS context variables with set_config', async () => {
88
+ const capturedValues = await withRlsContext(
89
+ db,
90
+ { user_id: 'user-123', tenant_id: 'tenant-456' },
91
+ async (trx) => {
92
+ // Read the session variables using current_setting
93
+ const userId = await sql<{ value: string }>`
94
94
  SELECT current_setting('app.user_id', true) as value
95
95
  `
96
- .execute(trx)
97
- .then((r) => r.rows[0]?.value);
96
+ .execute(trx)
97
+ .then((r) => r.rows[0]?.value);
98
98
 
99
- const tenantId = await sql<{ value: string }>`
99
+ const tenantId = await sql<{ value: string }>`
100
100
  SELECT current_setting('app.tenant_id', true) as value
101
101
  `
102
- .execute(trx)
103
- .then((r) => r.rows[0]?.value);
104
-
105
- return { userId, tenantId };
106
- },
107
- );
108
-
109
- expect(capturedValues.userId).toBe('user-123');
110
- expect(capturedValues.tenantId).toBe('tenant-456');
111
- });
112
-
113
- it('should use custom prefix when specified', async () => {
114
- const capturedValue = await withRlsContext(
115
- db,
116
- { user_id: 'user-123' },
117
- async (trx) => {
118
- const value = await sql<{ value: string }>`
102
+ .execute(trx)
103
+ .then((r) => r.rows[0]?.value);
104
+
105
+ return { userId, tenantId };
106
+ },
107
+ );
108
+
109
+ expect(capturedValues.userId).toBe('user-123');
110
+ expect(capturedValues.tenantId).toBe('tenant-456');
111
+ });
112
+
113
+ it('should use custom prefix when specified', async () => {
114
+ const capturedValue = await withRlsContext(
115
+ db,
116
+ { user_id: 'user-123' },
117
+ async (trx) => {
118
+ const value = await sql<{ value: string }>`
119
119
  SELECT current_setting('rls.user_id', true) as value
120
120
  `
121
- .execute(trx)
122
- .then((r) => r.rows[0]?.value);
123
-
124
- return value;
125
- },
126
- { prefix: 'rls' },
127
- );
128
-
129
- expect(capturedValue).toBe('user-123');
130
- });
131
-
132
- it('should skip null and undefined values', async () => {
133
- const capturedValues = await withRlsContext(
134
- db,
135
- {
136
- user_id: 'user-123',
137
- nullable_field: null,
138
- undefined_field: undefined,
139
- },
140
- async (trx) => {
141
- const userId = await sql<{ value: string }>`
121
+ .execute(trx)
122
+ .then((r) => r.rows[0]?.value);
123
+
124
+ return value;
125
+ },
126
+ { prefix: 'rls' },
127
+ );
128
+
129
+ expect(capturedValue).toBe('user-123');
130
+ });
131
+
132
+ it('should skip null and undefined values', async () => {
133
+ const capturedValues = await withRlsContext(
134
+ db,
135
+ {
136
+ user_id: 'user-123',
137
+ nullable_field: null,
138
+ undefined_field: undefined,
139
+ },
140
+ async (trx) => {
141
+ const userId = await sql<{ value: string }>`
142
142
  SELECT current_setting('app.user_id', true) as value
143
143
  `
144
- .execute(trx)
145
- .then((r) => r.rows[0]?.value);
144
+ .execute(trx)
145
+ .then((r) => r.rows[0]?.value);
146
146
 
147
- // These should return null since they weren't set
148
- const nullableField = await sql<{ value: string | null }>`
147
+ // These should return null since they weren't set
148
+ const nullableField = await sql<{ value: string | null }>`
149
149
  SELECT current_setting('app.nullable_field', true) as value
150
150
  `
151
- .execute(trx)
152
- .then((r) => r.rows[0]?.value);
151
+ .execute(trx)
152
+ .then((r) => r.rows[0]?.value);
153
153
 
154
- const undefinedField = await sql<{ value: string | null }>`
154
+ const undefinedField = await sql<{ value: string | null }>`
155
155
  SELECT current_setting('app.undefined_field', true) as value
156
156
  `
157
- .execute(trx)
158
- .then((r) => r.rows[0]?.value);
159
-
160
- return { userId, nullableField, undefinedField };
161
- },
162
- );
163
-
164
- expect(capturedValues.userId).toBe('user-123');
165
- expect(capturedValues.nullableField).toBeNull();
166
- expect(capturedValues.undefinedField).toBeNull();
167
- });
168
-
169
- it('should convert number values to strings', async () => {
170
- const capturedValue = await withRlsContext(
171
- db,
172
- { count: 42 },
173
- async (trx) => {
174
- return await sql<{ value: string }>`
157
+ .execute(trx)
158
+ .then((r) => r.rows[0]?.value);
159
+
160
+ return { userId, nullableField, undefinedField };
161
+ },
162
+ );
163
+
164
+ expect(capturedValues.userId).toBe('user-123');
165
+ expect(capturedValues.nullableField).toBeNull();
166
+ expect(capturedValues.undefinedField).toBeNull();
167
+ });
168
+
169
+ it('should convert number values to strings', async () => {
170
+ const capturedValue = await withRlsContext(
171
+ db,
172
+ { count: 42 },
173
+ async (trx) => {
174
+ return await sql<{ value: string }>`
175
175
  SELECT current_setting('app.count', true) as value
176
176
  `
177
- .execute(trx)
178
- .then((r) => r.rows[0]?.value);
179
- },
180
- );
181
-
182
- expect(capturedValue).toBe('42');
183
- });
184
-
185
- it('should convert boolean values to strings', async () => {
186
- const capturedValues = await withRlsContext(
187
- db,
188
- { is_admin: true, is_guest: false },
189
- async (trx) => {
190
- const isAdmin = await sql<{ value: string }>`
177
+ .execute(trx)
178
+ .then((r) => r.rows[0]?.value);
179
+ },
180
+ );
181
+
182
+ expect(capturedValue).toBe('42');
183
+ });
184
+
185
+ it('should convert boolean values to strings', async () => {
186
+ const capturedValues = await withRlsContext(
187
+ db,
188
+ { is_admin: true, is_guest: false },
189
+ async (trx) => {
190
+ const isAdmin = await sql<{ value: string }>`
191
191
  SELECT current_setting('app.is_admin', true) as value
192
192
  `
193
- .execute(trx)
194
- .then((r) => r.rows[0]?.value);
193
+ .execute(trx)
194
+ .then((r) => r.rows[0]?.value);
195
195
 
196
- const isGuest = await sql<{ value: string }>`
196
+ const isGuest = await sql<{ value: string }>`
197
197
  SELECT current_setting('app.is_guest', true) as value
198
198
  `
199
- .execute(trx)
200
- .then((r) => r.rows[0]?.value);
201
-
202
- return { isAdmin, isGuest };
203
- },
204
- );
205
-
206
- expect(capturedValues.isAdmin).toBe('true');
207
- expect(capturedValues.isGuest).toBe('false');
208
- });
209
-
210
- it('should propagate callback return value', async () => {
211
- const expectedResult = { id: 123, name: 'Test User' };
212
-
213
- const result = await withRlsContext(
214
- db,
215
- { user_id: 'user-123' },
216
- async () => expectedResult,
217
- );
218
-
219
- expect(result).toEqual(expectedResult);
220
- });
221
-
222
- it('should propagate errors from callback', async () => {
223
- const error = new Error('Query failed');
224
-
225
- await expect(
226
- withRlsContext(db, { user_id: 'user-123' }, async () => {
227
- throw error;
228
- }),
229
- ).rejects.toThrow('Query failed');
230
- });
231
-
232
- it('should handle empty context', async () => {
233
- const result = await withRlsContext(db, {}, async (trx) => {
234
- const order = await trx
235
- .insertInto('rlsTestOrders')
236
- .values({
237
- tenantId: 'tenant-1',
238
- userId: 'user-empty',
239
- amount: 50,
240
- })
241
- .returningAll()
242
- .executeTakeFirstOrThrow();
243
-
244
- return order;
245
- });
246
-
247
- expect(result.userId).toBe('user-empty');
248
- });
249
-
250
- it('should scope variables to transaction (not visible outside)', async () => {
251
- // Set a variable inside a transaction
252
- await withRlsContext(
253
- db,
254
- { scoped_var: 'inside-transaction' },
255
- async (trx) => {
256
- // Verify it's set inside
257
- const inside = await sql<{ value: string }>`
199
+ .execute(trx)
200
+ .then((r) => r.rows[0]?.value);
201
+
202
+ return { isAdmin, isGuest };
203
+ },
204
+ );
205
+
206
+ expect(capturedValues.isAdmin).toBe('true');
207
+ expect(capturedValues.isGuest).toBe('false');
208
+ });
209
+
210
+ it('should propagate callback return value', async () => {
211
+ const expectedResult = { id: 123, name: 'Test User' };
212
+
213
+ const result = await withRlsContext(
214
+ db,
215
+ { user_id: 'user-123' },
216
+ async () => expectedResult,
217
+ );
218
+
219
+ expect(result).toEqual(expectedResult);
220
+ });
221
+
222
+ it('should propagate errors from callback', async () => {
223
+ const error = new Error('Query failed');
224
+
225
+ await expect(
226
+ withRlsContext(db, { user_id: 'user-123' }, async () => {
227
+ throw error;
228
+ }),
229
+ ).rejects.toThrow('Query failed');
230
+ });
231
+
232
+ it('should handle empty context', async () => {
233
+ const result = await withRlsContext(db, {}, async (trx) => {
234
+ const order = await trx
235
+ .insertInto('rlsTestOrders')
236
+ .values({
237
+ tenantId: 'tenant-1',
238
+ userId: 'user-empty',
239
+ amount: 50,
240
+ })
241
+ .returningAll()
242
+ .executeTakeFirstOrThrow();
243
+
244
+ return order;
245
+ });
246
+
247
+ expect(result.userId).toBe('user-empty');
248
+ });
249
+
250
+ it('should scope variables to transaction (not visible outside)', async () => {
251
+ // Set a variable inside a transaction
252
+ await withRlsContext(
253
+ db,
254
+ { scoped_var: 'inside-transaction' },
255
+ async (trx) => {
256
+ // Verify it's set inside
257
+ const inside = await sql<{ value: string }>`
258
258
  SELECT current_setting('app.scoped_var', true) as value
259
259
  `
260
- .execute(trx)
261
- .then((r) => r.rows[0]?.value);
260
+ .execute(trx)
261
+ .then((r) => r.rows[0]?.value);
262
262
 
263
- expect(inside).toBe('inside-transaction');
264
- return 'done';
265
- },
266
- );
263
+ expect(inside).toBe('inside-transaction');
264
+ return 'done';
265
+ },
266
+ );
267
267
 
268
- // Verify it's not the transaction value outside
269
- // PostgreSQL returns empty string for unset custom variables (not null)
270
- const outside = await sql<{ value: string | null }>`
268
+ // Verify it's not the transaction value outside
269
+ // PostgreSQL returns empty string for unset custom variables (not null)
270
+ const outside = await sql<{ value: string | null }>`
271
271
  SELECT current_setting('app.scoped_var', true) as value
272
272
  `
273
- .execute(db)
274
- .then((r) => r.rows[0]?.value);
275
-
276
- // The value should NOT be 'inside-transaction' - it's cleared/reset
277
- expect(outside).not.toBe('inside-transaction');
278
- // PostgreSQL returns empty string for missing custom settings
279
- expect(outside === '' || outside === null).toBe(true);
280
- });
281
-
282
- it('should reuse existing transaction', async () => {
283
- const result = await withRlsContext(
284
- db,
285
- { outer_var: 'outer' },
286
- async (outerTrx) => {
287
- // Nested withRlsContext should reuse the same transaction
288
- const innerResult = await withRlsContext(
289
- outerTrx,
290
- { inner_var: 'inner' },
291
- async (innerTrx) => {
292
- // Both variables should be visible in nested context
293
- const outerVar = await sql<{ value: string }>`
273
+ .execute(db)
274
+ .then((r) => r.rows[0]?.value);
275
+
276
+ // The value should NOT be 'inside-transaction' - it's cleared/reset
277
+ expect(outside).not.toBe('inside-transaction');
278
+ // PostgreSQL returns empty string for missing custom settings
279
+ expect(outside === '' || outside === null).toBe(true);
280
+ });
281
+
282
+ it('should reuse existing transaction', async () => {
283
+ const result = await withRlsContext(
284
+ db,
285
+ { outer_var: 'outer' },
286
+ async (outerTrx) => {
287
+ // Nested withRlsContext should reuse the same transaction
288
+ const innerResult = await withRlsContext(
289
+ outerTrx,
290
+ { inner_var: 'inner' },
291
+ async (innerTrx) => {
292
+ // Both variables should be visible in nested context
293
+ const outerVar = await sql<{ value: string }>`
294
294
  SELECT current_setting('app.outer_var', true) as value
295
295
  `
296
- .execute(innerTrx)
297
- .then((r) => r.rows[0]?.value);
296
+ .execute(innerTrx)
297
+ .then((r) => r.rows[0]?.value);
298
298
 
299
- const innerVar = await sql<{ value: string }>`
299
+ const innerVar = await sql<{ value: string }>`
300
300
  SELECT current_setting('app.inner_var', true) as value
301
301
  `
302
- .execute(innerTrx)
303
- .then((r) => r.rows[0]?.value);
304
-
305
- return { outerVar, innerVar };
306
- },
307
- );
308
-
309
- return innerResult;
310
- },
311
- );
312
-
313
- expect(result.outerVar).toBe('outer');
314
- expect(result.innerVar).toBe('inner');
315
- });
316
-
317
- it('should rollback on error including RLS context', async () => {
318
- try {
319
- await withRlsContext(
320
- db,
321
- { rollback_test: 'should-rollback' },
322
- async (trx) => {
323
- await trx
324
- .insertInto('rlsTestOrders')
325
- .values({
326
- tenantId: 'tenant-rollback',
327
- userId: 'user-rollback',
328
- amount: 999,
329
- })
330
- .execute();
331
-
332
- throw new Error('Force rollback');
333
- },
334
- );
335
- } catch {
336
- // Expected error
337
- }
338
-
339
- // Verify the insert was rolled back
340
- const orders = await db
341
- .selectFrom('rlsTestOrders')
342
- .selectAll()
343
- .where('tenantId', '=', 'tenant-rollback')
344
- .execute();
345
-
346
- expect(orders).toHaveLength(0);
347
- });
348
-
349
- it('should pass transaction settings (isolation level)', async () => {
350
- const result = await withRlsContext(
351
- db,
352
- { user_id: 'user-serializable' },
353
- async (trx) => {
354
- // This query should work under any isolation level
355
- const order = await trx
356
- .insertInto('rlsTestOrders')
357
- .values({
358
- tenantId: 'tenant-serializable',
359
- userId: 'user-serializable',
360
- amount: 200,
361
- })
362
- .returningAll()
363
- .executeTakeFirstOrThrow();
364
-
365
- return order;
366
- },
367
- { settings: { isolationLevel: 'serializable' } },
368
- );
369
-
370
- expect(result.userId).toBe('user-serializable');
371
- });
372
-
373
- it('should support different return types', async () => {
374
- // Number
375
- const numResult = await withRlsContext(db, {}, async () => 42);
376
- expect(numResult).toBe(42);
377
-
378
- // String
379
- const strResult = await withRlsContext(db, {}, async () => 'test');
380
- expect(strResult).toBe('test');
381
-
382
- // Boolean
383
- const boolResult = await withRlsContext(db, {}, async () => true);
384
- expect(boolResult).toBe(true);
385
-
386
- // Array
387
- const arrResult = await withRlsContext(db, {}, async () => [1, 2, 3]);
388
- expect(arrResult).toEqual([1, 2, 3]);
389
-
390
- // Object
391
- const objResult = await withRlsContext(db, {}, async () => ({
392
- key: 'value',
393
- }));
394
- expect(objResult).toEqual({ key: 'value' });
395
- });
396
- });
397
-
398
- describe('RLS_BYPASS', () => {
399
- it('should be a unique symbol', () => {
400
- expect(typeof RLS_BYPASS).toBe('symbol');
401
- expect(RLS_BYPASS.description).toBe('geekmidas.rls.bypass');
402
- });
403
-
404
- it('should be the same symbol across imports', () => {
405
- const symbolFromGlobal = Symbol.for('geekmidas.rls.bypass');
406
- expect(RLS_BYPASS).toBe(symbolFromGlobal);
407
- });
408
- });
409
-
410
- describe('RlsContext type', () => {
411
- it('should accept string values', () => {
412
- const context: RlsContext = {
413
- user_id: 'user-123',
414
- tenant_id: 'tenant-456',
415
- };
416
- expect(context.user_id).toBe('user-123');
417
- });
418
-
419
- it('should accept number values', () => {
420
- const context: RlsContext = {
421
- count: 42,
422
- decimal: 3.14,
423
- };
424
- expect(context.count).toBe(42);
425
- });
426
-
427
- it('should accept boolean values', () => {
428
- const context: RlsContext = {
429
- is_admin: true,
430
- is_guest: false,
431
- };
432
- expect(context.is_admin).toBe(true);
433
- });
434
-
435
- it('should accept null and undefined values', () => {
436
- const context: RlsContext = {
437
- nullable: null,
438
- optional: undefined,
439
- };
440
- expect(context.nullable).toBe(null);
441
- expect(context.optional).toBe(undefined);
442
- });
443
- });
302
+ .execute(innerTrx)
303
+ .then((r) => r.rows[0]?.value);
304
+
305
+ return { outerVar, innerVar };
306
+ },
307
+ );
308
+
309
+ return innerResult;
310
+ },
311
+ );
312
+
313
+ expect(result.outerVar).toBe('outer');
314
+ expect(result.innerVar).toBe('inner');
315
+ });
316
+
317
+ it('should rollback on error including RLS context', async () => {
318
+ try {
319
+ await withRlsContext(
320
+ db,
321
+ { rollback_test: 'should-rollback' },
322
+ async (trx) => {
323
+ await trx
324
+ .insertInto('rlsTestOrders')
325
+ .values({
326
+ tenantId: 'tenant-rollback',
327
+ userId: 'user-rollback',
328
+ amount: 999,
329
+ })
330
+ .execute();
331
+
332
+ throw new Error('Force rollback');
333
+ },
334
+ );
335
+ } catch {
336
+ // Expected error
337
+ }
338
+
339
+ // Verify the insert was rolled back
340
+ const orders = await db
341
+ .selectFrom('rlsTestOrders')
342
+ .selectAll()
343
+ .where('tenantId', '=', 'tenant-rollback')
344
+ .execute();
345
+
346
+ expect(orders).toHaveLength(0);
347
+ });
348
+
349
+ it('should pass transaction settings (isolation level)', async () => {
350
+ const result = await withRlsContext(
351
+ db,
352
+ { user_id: 'user-serializable' },
353
+ async (trx) => {
354
+ // This query should work under any isolation level
355
+ const order = await trx
356
+ .insertInto('rlsTestOrders')
357
+ .values({
358
+ tenantId: 'tenant-serializable',
359
+ userId: 'user-serializable',
360
+ amount: 200,
361
+ })
362
+ .returningAll()
363
+ .executeTakeFirstOrThrow();
364
+
365
+ return order;
366
+ },
367
+ { settings: { isolationLevel: 'serializable' } },
368
+ );
369
+
370
+ expect(result.userId).toBe('user-serializable');
371
+ });
372
+
373
+ it('should support different return types', async () => {
374
+ // Number
375
+ const numResult = await withRlsContext(db, {}, async () => 42);
376
+ expect(numResult).toBe(42);
377
+
378
+ // String
379
+ const strResult = await withRlsContext(db, {}, async () => 'test');
380
+ expect(strResult).toBe('test');
381
+
382
+ // Boolean
383
+ const boolResult = await withRlsContext(db, {}, async () => true);
384
+ expect(boolResult).toBe(true);
385
+
386
+ // Array
387
+ const arrResult = await withRlsContext(db, {}, async () => [1, 2, 3]);
388
+ expect(arrResult).toEqual([1, 2, 3]);
389
+
390
+ // Object
391
+ const objResult = await withRlsContext(db, {}, async () => ({
392
+ key: 'value',
393
+ }));
394
+ expect(objResult).toEqual({ key: 'value' });
395
+ });
396
+ });
397
+
398
+ describe('RLS_BYPASS', () => {
399
+ it('should be a unique symbol', () => {
400
+ expect(typeof RLS_BYPASS).toBe('symbol');
401
+ expect(RLS_BYPASS.description).toBe('geekmidas.rls.bypass');
402
+ });
403
+
404
+ it('should be the same symbol across imports', () => {
405
+ const symbolFromGlobal = Symbol.for('geekmidas.rls.bypass');
406
+ expect(RLS_BYPASS).toBe(symbolFromGlobal);
407
+ });
408
+ });
409
+
410
+ describe('RlsContext type', () => {
411
+ it('should accept string values', () => {
412
+ const context: RlsContext = {
413
+ user_id: 'user-123',
414
+ tenant_id: 'tenant-456',
415
+ };
416
+ expect(context.user_id).toBe('user-123');
417
+ });
418
+
419
+ it('should accept number values', () => {
420
+ const context: RlsContext = {
421
+ count: 42,
422
+ decimal: 3.14,
423
+ };
424
+ expect(context.count).toBe(42);
425
+ });
426
+
427
+ it('should accept boolean values', () => {
428
+ const context: RlsContext = {
429
+ is_admin: true,
430
+ is_guest: false,
431
+ };
432
+ expect(context.is_admin).toBe(true);
433
+ });
434
+
435
+ it('should accept null and undefined values', () => {
436
+ const context: RlsContext = {
437
+ nullable: null,
438
+ optional: undefined,
439
+ };
440
+ expect(context.nullable).toBe(null);
441
+ expect(context.optional).toBe(undefined);
442
+ });
443
+ });
444
444
  });