@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.
- package/dist/kysely/pagination.cjs +83 -0
- package/dist/kysely/pagination.cjs.map +1 -0
- package/dist/kysely/pagination.d.cts +79 -0
- package/dist/kysely/pagination.d.cts.map +1 -0
- package/dist/kysely/pagination.d.mts +79 -0
- package/dist/kysely/pagination.d.mts.map +1 -0
- package/dist/kysely/pagination.mjs +79 -0
- package/dist/kysely/pagination.mjs.map +1 -0
- package/dist/kysely-8WPSKCZG.cjs.map +1 -1
- package/dist/{kysely-0FOi6ZdO.d.cts → kysely-D3eeuId4.d.cts} +3 -1
- package/dist/kysely-D3eeuId4.d.cts.map +1 -0
- package/dist/{kysely-Di1LVvL2.d.mts → kysely-DUdsB0BP.d.mts} +3 -1
- package/dist/kysely-DUdsB0BP.d.mts.map +1 -0
- package/dist/kysely-DmfA94RY.mjs.map +1 -1
- package/dist/kysely.d.cts +1 -1
- package/dist/kysely.d.mts +1 -1
- package/dist/rls.cjs.map +1 -1
- package/dist/rls.d.cts +2 -1
- package/dist/rls.d.cts.map +1 -0
- package/dist/rls.d.mts +2 -1
- package/dist/rls.d.mts.map +1 -0
- package/dist/rls.mjs.map +1 -1
- package/package.json +6 -1
- package/src/__tests__/kysely.integration.spec.ts +729 -729
- package/src/__tests__/kysely.spec.ts +388 -388
- package/src/__tests__/rls.spec.ts +399 -399
- package/src/kysely/__tests__/pagination.integration.spec.ts +341 -0
- package/src/kysely/pagination.ts +150 -0
- package/src/kysely.ts +19 -19
- package/src/rls.ts +31 -31
- package/tsconfig.json +9 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
97
|
-
|
|
96
|
+
.execute(trx)
|
|
97
|
+
.then((r) => r.rows[0]?.value);
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
const tenantId = await sql<{ value: string }>`
|
|
100
100
|
SELECT current_setting('app.tenant_id', true) as value
|
|
101
101
|
`
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
145
|
-
|
|
144
|
+
.execute(trx)
|
|
145
|
+
.then((r) => r.rows[0]?.value);
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
152
|
-
|
|
151
|
+
.execute(trx)
|
|
152
|
+
.then((r) => r.rows[0]?.value);
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
const undefinedField = await sql<{ value: string | null }>`
|
|
155
155
|
SELECT current_setting('app.undefined_field', true) as value
|
|
156
156
|
`
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
194
|
-
|
|
193
|
+
.execute(trx)
|
|
194
|
+
.then((r) => r.rows[0]?.value);
|
|
195
195
|
|
|
196
|
-
|
|
196
|
+
const isGuest = await sql<{ value: string }>`
|
|
197
197
|
SELECT current_setting('app.is_guest', true) as value
|
|
198
198
|
`
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
261
|
-
|
|
260
|
+
.execute(trx)
|
|
261
|
+
.then((r) => r.rows[0]?.value);
|
|
262
262
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
263
|
+
expect(inside).toBe('inside-transaction');
|
|
264
|
+
return 'done';
|
|
265
|
+
},
|
|
266
|
+
);
|
|
267
267
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
297
|
-
|
|
296
|
+
.execute(innerTrx)
|
|
297
|
+
.then((r) => r.rows[0]?.value);
|
|
298
298
|
|
|
299
|
-
|
|
299
|
+
const innerVar = await sql<{ value: string }>`
|
|
300
300
|
SELECT current_setting('app.inner_var', true) as value
|
|
301
301
|
`
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
});
|