@kysera/rls 0.7.2 → 0.7.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @kysera/rls
2
2
 
3
- > **Row-Level Security Plugin for Kysera** - Declarative authorization policies with automatic query transformation and AsyncLocalStorage-based context management.
3
+ > **Row-Level Security Plugin for Kysera (v0.7.3)** - Declarative authorization policies with automatic query transformation through @kysera/executor's Unified Execution Layer and AsyncLocalStorage-based context management.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@kysera/rls.svg)](https://www.npmjs.com/package/@kysera/rls)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
@@ -19,13 +19,14 @@ RLS controls access to individual rows in database tables based on user context.
19
19
  **Key Features:**
20
20
 
21
21
  - **Declarative Policy DSL** - Define rules with `allow`, `deny`, `filter`, and `validate` builders
22
- - **Automatic Query Transformation** - SELECT queries are filtered automatically via `interceptQuery`
23
- - **Repository Extensions** - Wraps mutation methods via `extendRepository` for policy enforcement
24
- - **Type-Safe Context** - Full TypeScript inference for user context and policies
22
+ - **Automatic Query Transformation** - SELECT queries are filtered automatically via `interceptQuery` hook
23
+ - **Repository Extensions** - Wraps mutation methods via `extendRepository` hook for policy enforcement
24
+ - **Type-Safe Context** - Full TypeScript inference with reduced `any` usage through type utilities
25
25
  - **Multi-Tenant Isolation** - Built-in patterns for SaaS tenant separation
26
- - **Plugin Architecture** - Works with both Repository and DAL patterns via `@kysera/executor`
26
+ - **Plugin Architecture** - Works with both Repository and DAL patterns via @kysera/executor's Unified Execution Layer
27
27
  - **Zero Runtime Overhead** - Policies compiled at initialization
28
28
  - **AsyncLocalStorage Context** - Request-scoped context without prop drilling
29
+ - **Optional Dependency** - Listed in peerDependencies with `optional: true` for @kysera/repository
29
30
 
30
31
  ---
31
32
 
@@ -40,10 +41,13 @@ yarn add @kysera/rls kysely
40
41
  ```
41
42
 
42
43
  **Dependencies:**
44
+
43
45
  - `kysely` >= 0.28.8 (peer dependency)
44
- - `@kysera/core` - Core utilities (auto-installed)
45
- - `@kysera/executor` - Plugin execution layer (auto-installed)
46
- - `@kysera/repository` or `@kysera/dal` - For Repository or DAL patterns (install as needed)
46
+ - `@kysera/core` >= 0.7.0 - Core utilities (auto-installed)
47
+ - `@kysera/executor` >= 0.7.0 - Unified Execution Layer (auto-installed)
48
+ - `@kysera/repository` >= 0.7.0 or `@kysera/dal` >= 0.7.0 - For Repository or DAL patterns (install as needed)
49
+
50
+ **Note:** @kysera/rls is listed in peerDependencies with `optional: true` for @kysera/repository, allowing flexible installation based on your needs.
47
51
 
48
52
  ---
49
53
 
@@ -52,17 +56,17 @@ yarn add @kysera/rls kysely
52
56
  ### 1. Define RLS Schema
53
57
 
54
58
  ```typescript
55
- import { defineRLSSchema, filter, allow, validate } from '@kysera/rls';
59
+ import { defineRLSSchema, filter, allow, validate } from '@kysera/rls'
56
60
 
57
61
  interface Database {
58
62
  posts: {
59
- id: number;
60
- title: string;
61
- content: string;
62
- author_id: number;
63
- tenant_id: number;
64
- status: 'draft' | 'published';
65
- };
63
+ id: number
64
+ title: string
65
+ content: string
66
+ author_id: number
67
+ tenant_id: number
68
+ status: 'draft' | 'published'
69
+ }
66
70
  }
67
71
 
68
72
  const rlsSchema = defineRLSSchema<Database>({
@@ -72,44 +76,47 @@ const rlsSchema = defineRLSSchema<Database>({
72
76
  filter('read', ctx => ({ tenant_id: ctx.auth.tenantId })),
73
77
 
74
78
  // Authors can edit their own posts
75
- allow(['update', 'delete'], ctx =>
76
- ctx.auth.userId === ctx.row.author_id
77
- ),
79
+ allow(['update', 'delete'], ctx => ctx.auth.userId === ctx.row.author_id),
78
80
 
79
81
  // Validate new posts belong to user's tenant
80
- validate('create', ctx =>
81
- ctx.data.tenant_id === ctx.auth.tenantId
82
- ),
82
+ validate('create', ctx => ctx.data.tenant_id === ctx.auth.tenantId)
83
83
  ],
84
- defaultDeny: true, // Require explicit allow
85
- },
86
- });
84
+ defaultDeny: true // Require explicit allow
85
+ }
86
+ })
87
87
  ```
88
88
 
89
- ### 2. Create Plugin Container with RLS Plugin
89
+ ### 2. Register Plugin with Unified Execution Layer
90
90
 
91
91
  ```typescript
92
- import { createORM } from '@kysera/repository';
93
- import { rlsPlugin, rlsContext } from '@kysera/rls';
94
- import { Kysely, PostgresDialect } from 'kysely';
92
+ import { createExecutor } from '@kysera/executor'
93
+ import { createORM } from '@kysera/repository'
94
+ import { rlsPlugin, rlsContext } from '@kysera/rls'
95
+ import { Kysely, PostgresDialect } from 'kysely'
95
96
 
96
97
  const db = new Kysely<Database>({
97
- dialect: new PostgresDialect({ /* config */ }),
98
- });
98
+ dialect: new PostgresDialect({
99
+ /* config */
100
+ })
101
+ })
99
102
 
100
- const orm = await createORM(db, [
101
- rlsPlugin({ schema: rlsSchema }),
102
- ]);
103
+ // Step 1: Create executor with RLS plugin
104
+ const executor = await createExecutor(db, [
105
+ rlsPlugin({ schema: rlsSchema })
106
+ ])
107
+
108
+ // Step 2: Create ORM with plugin-enabled executor
109
+ const orm = await createORM(executor, [])
103
110
  ```
104
111
 
105
112
  ### 3. Execute Queries within RLS Context
106
113
 
107
114
  ```typescript
108
- import { rlsContext } from '@kysera/rls';
115
+ import { rlsContext } from '@kysera/rls'
109
116
 
110
117
  // In your request handler
111
118
  app.use(async (req, res, next) => {
112
- const user = await authenticate(req);
119
+ const user = await authenticate(req)
113
120
 
114
121
  await rlsContext.runAsync(
115
122
  {
@@ -117,51 +124,63 @@ app.use(async (req, res, next) => {
117
124
  userId: user.id,
118
125
  tenantId: user.tenantId,
119
126
  roles: user.roles,
120
- isSystem: false,
127
+ isSystem: false
121
128
  },
122
- timestamp: new Date(),
129
+ timestamp: new Date()
123
130
  },
124
131
  async () => {
125
132
  // All queries automatically filtered by policies
126
- const posts = await orm.posts.findAll();
127
- res.json(posts);
133
+ const posts = await orm.posts.findAll()
134
+ res.json(posts)
128
135
  }
129
- );
130
- });
136
+ )
137
+ })
131
138
  ```
132
139
 
133
140
  ---
134
141
 
135
142
  ## Plugin Architecture
136
143
 
137
- ### Integration with @kysera/executor
144
+ ### Integration with @kysera/executor's Unified Execution Layer
138
145
 
139
- The RLS plugin is built on `@kysera/executor`, which provides a unified plugin system that works with both Repository and DAL patterns.
146
+ The RLS plugin is built on @kysera/executor's Unified Execution Layer, which provides seamless plugin support that works with both Repository and DAL patterns.
140
147
 
141
148
  **Plugin Metadata:**
142
149
 
143
150
  ```typescript
144
151
  {
145
152
  name: '@kysera/rls',
146
- version: '0.7.0',
153
+ version: '0.7.3',
147
154
  priority: 50, // Runs after soft-delete (0), before audit (100)
148
155
  dependencies: [],
149
156
  }
150
157
  ```
151
158
 
159
+ **Type Utilities:**
160
+
161
+ The plugin uses type utilities to reduce `any` usage and improve type safety:
162
+ - Conditional types for precise type inference
163
+ - Generic constraints for database schemas
164
+ - Utility types for context and policy definitions
165
+
152
166
  ### How It Works
153
167
 
154
- The RLS plugin implements two key hooks from the `@kysera/executor` plugin system:
168
+ The RLS plugin implements two key hooks from @kysera/executor's plugin system:
155
169
 
156
170
  #### 1. `interceptQuery` - Query Filtering (SELECT)
157
171
 
158
- The `interceptQuery` hook intercepts all query builder operations to apply RLS filtering:
172
+ Registered via `createExecutor()`, the `interceptQuery` hook intercepts all query builder operations to apply RLS filtering:
159
173
 
160
174
  ```typescript
161
- // When you execute a SELECT query:
162
- const posts = await orm.posts.findAll();
175
+ // Step 1: Register plugin with Unified Execution Layer
176
+ const executor = await createExecutor(db, [
177
+ rlsPlugin({ schema: rlsSchema })
178
+ ])
179
+
180
+ // Step 2: Execute a SELECT query
181
+ const posts = await orm.posts.findAll()
163
182
 
164
- // The plugin interceptQuery hook:
183
+ // Step 3: The plugin interceptQuery hook:
165
184
  // 1. Checks for RLS context (rlsContext.getContextOrNull())
166
185
  // 2. Checks if system user (ctx.auth.isSystem) or bypass role
167
186
  // 3. Applies filter policies as WHERE conditions via SelectTransformer
@@ -170,19 +189,23 @@ const posts = await orm.posts.findAll();
170
189
  ```
171
190
 
172
191
  **Key behavior:**
192
+
173
193
  - SELECT operations: Policies are applied immediately as WHERE clauses
174
194
  - INSERT/UPDATE/DELETE: Marked for validation (actual enforcement in `extendRepository`)
175
195
  - Skip conditions: `skipTables`, `metadata['skipRLS']`, `requireContext`, system user, bypass roles
176
196
 
177
197
  #### 2. `extendRepository` - Mutation Enforcement (CREATE/UPDATE/DELETE)
178
198
 
179
- The `extendRepository` hook wraps repository mutation methods to enforce RLS policies:
199
+ Registered via `createExecutor()`, the `extendRepository` hook wraps repository mutation methods to enforce RLS policies:
180
200
 
181
201
  ```typescript
182
- // When you call a mutation:
183
- await repo.update(postId, { title: 'New Title' });
202
+ // Step 1: Plugin registered with Unified Execution Layer
203
+ const executor = await createExecutor(db, [rlsPlugin({ schema: rlsSchema })])
184
204
 
185
- // The plugin extendRepository hook:
205
+ // Step 2: Call a mutation
206
+ await repo.update(postId, { title: 'New Title' })
207
+
208
+ // Step 3: The plugin extendRepository hook:
186
209
  // 1. Wraps create/update/delete methods
187
210
  // 2. Fetches existing row using getRawDb() (bypasses RLS filtering)
188
211
  // 3. Evaluates allow/deny policies via MutationGuard
@@ -191,7 +214,7 @@ await repo.update(postId, { title: 'New Title' });
191
214
  // 6. Adds withoutRLS() and canAccess() utility methods
192
215
  ```
193
216
 
194
- **Why use `getRawDb()`?** To prevent infinite recursion - we need to fetch the existing row without triggering RLS filtering. The `getRawDb()` function from `@kysera/executor` returns the original Kysely instance that bypasses all plugin hooks.
217
+ **Why use `getRawDb()`?** To prevent infinite recursion - we need to fetch the existing row without triggering RLS filtering. The `getRawDb()` function from @kysera/executor returns the original Kysely instance that bypasses all plugin hooks.
195
218
 
196
219
  ---
197
220
 
@@ -247,7 +270,7 @@ filter('read', ctx =>
247
270
  // Multiple conditions
248
271
  filter('read', ctx => ({
249
272
  organization_id: ctx.auth.organizationIds?.[0],
250
- deleted_at: null,
273
+ deleted_at: null
251
274
  }))
252
275
  ```
253
276
 
@@ -257,31 +280,28 @@ Validates data during CREATE/UPDATE operations.
257
280
 
258
281
  ```typescript
259
282
  // Validate tenant ownership
260
- validate('create', ctx =>
261
- ctx.data.tenant_id === ctx.auth.tenantId
262
- )
283
+ validate('create', ctx => ctx.data.tenant_id === ctx.auth.tenantId)
263
284
 
264
285
  // Validate status transitions
265
286
  validate('update', ctx => {
266
287
  const validTransitions = {
267
288
  draft: ['published', 'archived'],
268
289
  published: ['archived'],
269
- archived: [],
270
- };
271
- return !ctx.data.status ||
272
- validTransitions[ctx.row.status]?.includes(ctx.data.status);
290
+ archived: []
291
+ }
292
+ return !ctx.data.status || validTransitions[ctx.row.status]?.includes(ctx.data.status)
273
293
  })
274
294
  ```
275
295
 
276
296
  ### Operations
277
297
 
278
- | Operation | SQL | Description |
279
- |-----------|-----|-------------|
280
- | `read` | SELECT | Control what users can view |
281
- | `create` | INSERT | Control what users can create |
282
- | `update` | UPDATE | Control what users can modify |
283
- | `delete` | DELETE | Control what users can remove |
284
- | `all` | All | Apply to all operations |
298
+ | Operation | SQL | Description |
299
+ | --------- | ------ | ----------------------------- |
300
+ | `read` | SELECT | Control what users can view |
301
+ | `create` | INSERT | Control what users can create |
302
+ | `update` | UPDATE | Control what users can modify |
303
+ | `delete` | DELETE | Control what users can remove |
304
+ | `all` | All | Apply to all operations |
285
305
 
286
306
  ```typescript
287
307
  // Single operation
@@ -316,6 +336,7 @@ deny('all', ctx => ctx.auth.suspended)
316
336
  ```
317
337
 
318
338
  **Priority System:**
339
+
319
340
  - Higher priority = evaluated first
320
341
  - Deny policies default to priority `100`
321
342
  - Allow/filter/validate default to priority `0`
@@ -334,10 +355,10 @@ defineRLSSchema<Database>({
334
355
  allow('read', ctx => ctx.auth.premium, { priority: 50 }),
335
356
 
336
357
  // Default priority
337
- allow('read', ctx => ctx.row.public),
338
- ],
339
- },
340
- });
358
+ allow('read', ctx => ctx.row.public)
359
+ ]
360
+ }
361
+ })
341
362
  ```
342
363
 
343
364
  ---
@@ -365,8 +386,8 @@ allow('read', ctx => ctx.auth.verified, {
365
386
 
366
387
  // Async condition
367
388
  allow('update', async ctx => {
368
- const hasPermission = await checkPermission(ctx.auth.userId, 'posts:edit');
369
- return hasPermission;
389
+ const hasPermission = await checkPermission(ctx.auth.userId, 'posts:edit')
390
+ return hasPermission
370
391
  })
371
392
  ```
372
393
 
@@ -405,9 +426,9 @@ filter('read', ctx => ({
405
426
  // Dynamic filter
406
427
  filter('read', ctx => {
407
428
  if (ctx.auth.roles.includes('admin')) {
408
- return {}; // No filtering
429
+ return {} // No filtering
409
430
  }
410
- return { status: 'published', public: true };
431
+ return { status: 'published', public: true }
411
432
  })
412
433
 
413
434
  // With hints
@@ -426,8 +447,8 @@ validate('create', ctx => ctx.data.tenant_id === ctx.auth.tenantId)
426
447
 
427
448
  // Validate update
428
449
  validate('update', ctx => {
429
- const allowedFields = ['title', 'content', 'tags'];
430
- return Object.keys(ctx.data).every(key => allowedFields.includes(key));
450
+ const allowedFields = ['title', 'content', 'tags']
451
+ return Object.keys(ctx.data).every(key => allowedFields.includes(key))
431
452
  })
432
453
 
433
454
  // Both create and update
@@ -439,18 +460,18 @@ validate('all', ctx => !ctx.data.price || ctx.data.price >= 0)
439
460
  ```typescript
440
461
  interface PolicyOptions {
441
462
  /** Policy name for debugging */
442
- name?: string;
463
+ name?: string
443
464
 
444
465
  /** Priority (higher runs first) */
445
- priority?: number;
466
+ priority?: number
446
467
 
447
468
  /** Performance hints */
448
469
  hints?: {
449
- indexColumns?: string[];
450
- selectivity?: 'high' | 'medium' | 'low';
451
- leakproof?: boolean;
452
- stable?: boolean;
453
- };
470
+ indexColumns?: string[]
471
+ selectivity?: 'high' | 'medium' | 'low'
472
+ leakproof?: boolean
473
+ stable?: boolean
474
+ }
454
475
  }
455
476
  ```
456
477
 
@@ -465,28 +486,29 @@ The RLS context is stored and managed using AsyncLocalStorage, providing automat
465
486
  ```typescript
466
487
  interface RLSContext<TUser = unknown, TMeta = unknown> {
467
488
  auth: {
468
- userId: string | number; // Required
469
- roles: string[]; // Required
470
- tenantId?: string | number; // Optional
471
- organizationIds?: (string | number)[];
472
- permissions?: string[];
473
- attributes?: Record<string, unknown>;
474
- user?: TUser;
475
- isSystem?: boolean; // Default: false
476
- };
489
+ userId: string | number // Required
490
+ roles: string[] // Required
491
+ tenantId?: string | number // Optional
492
+ organizationIds?: (string | number)[]
493
+ permissions?: string[]
494
+ attributes?: Record<string, unknown>
495
+ user?: TUser
496
+ isSystem?: boolean // Default: false
497
+ }
477
498
  request?: {
478
- requestId?: string;
479
- ipAddress?: string;
480
- userAgent?: string;
481
- timestamp: Date;
482
- headers?: Record<string, string>;
483
- };
484
- meta?: TMeta;
485
- timestamp: Date;
499
+ requestId?: string
500
+ ipAddress?: string
501
+ userAgent?: string
502
+ timestamp: Date
503
+ headers?: Record<string, string>
504
+ }
505
+ meta?: TMeta
506
+ timestamp: Date
486
507
  }
487
508
  ```
488
509
 
489
510
  **Context Storage:** The plugin uses `AsyncLocalStorage` internally to store the RLS context, which:
511
+
490
512
  - Automatically propagates through async/await chains
491
513
  - Is isolated per request (no cross-contamination)
492
514
  - Requires no manual passing of context objects
@@ -507,18 +529,18 @@ await rlsContext.runAsync(
507
529
  userId: 123,
508
530
  roles: ['user'],
509
531
  tenantId: 'acme-corp',
510
- isSystem: false,
532
+ isSystem: false
511
533
  },
512
- timestamp: new Date(),
534
+ timestamp: new Date()
513
535
  },
514
536
  async () => {
515
537
  // All queries within this block use this context
516
- const posts = await orm.posts.findAll();
538
+ const posts = await orm.posts.findAll()
517
539
 
518
540
  // Context propagates through async operations
519
- await orm.posts.create({ title: 'New Post' });
541
+ await orm.posts.create({ title: 'New Post' })
520
542
  }
521
- );
543
+ )
522
544
  ```
523
545
 
524
546
  #### `rlsContext.run(context, fn)`
@@ -528,8 +550,8 @@ Run synchronous function within RLS context:
528
550
  ```typescript
529
551
  const result = rlsContext.run(context, () => {
530
552
  // Synchronous operations
531
- return someValue;
532
- });
553
+ return someValue
554
+ })
533
555
  ```
534
556
 
535
557
  #### `createRLSContext(options)`
@@ -537,30 +559,30 @@ const result = rlsContext.run(context, () => {
537
559
  Create and validate RLS context with proper defaults:
538
560
 
539
561
  ```typescript
540
- import { createRLSContext } from '@kysera/rls';
562
+ import { createRLSContext } from '@kysera/rls'
541
563
 
542
564
  const ctx = createRLSContext({
543
565
  auth: {
544
566
  userId: 123,
545
567
  roles: ['user', 'editor'],
546
568
  tenantId: 'acme-corp',
547
- permissions: ['posts:read', 'posts:write'],
569
+ permissions: ['posts:read', 'posts:write']
548
570
  },
549
571
  // Optional request context
550
572
  request: {
551
573
  requestId: 'req-abc123',
552
574
  ipAddress: '192.168.1.1',
553
- timestamp: new Date(),
575
+ timestamp: new Date()
554
576
  },
555
577
  // Optional metadata
556
578
  meta: {
557
- featureFlags: ['beta_access'],
558
- },
559
- });
579
+ featureFlags: ['beta_access']
580
+ }
581
+ })
560
582
 
561
583
  await rlsContext.runAsync(ctx, async () => {
562
584
  // ...
563
- });
585
+ })
564
586
  ```
565
587
 
566
588
  #### Context Helper Methods
@@ -569,10 +591,10 @@ The `rlsContext` singleton provides helper methods for accessing context:
569
591
 
570
592
  ```typescript
571
593
  // Get current context (throws RLSContextError if not set)
572
- const ctx = rlsContext.getContext();
594
+ const ctx = rlsContext.getContext()
573
595
 
574
596
  // Get context or null (safe, no throw)
575
- const ctx = rlsContext.getContextOrNull();
597
+ const ctx = rlsContext.getContextOrNull()
576
598
 
577
599
  // Check if running within context
578
600
  if (rlsContext.hasContext()) {
@@ -580,13 +602,13 @@ if (rlsContext.hasContext()) {
580
602
  }
581
603
 
582
604
  // Get auth context (throws if no context)
583
- const auth = rlsContext.getAuth();
605
+ const auth = rlsContext.getAuth()
584
606
 
585
607
  // Get user ID (throws if no context)
586
- const userId = rlsContext.getUserId();
608
+ const userId = rlsContext.getUserId()
587
609
 
588
610
  // Get tenant ID (throws if no context)
589
- const tenantId = rlsContext.getTenantId();
611
+ const tenantId = rlsContext.getTenantId()
590
612
 
591
613
  // Check if user has role
592
614
  if (rlsContext.hasRole('admin')) {
@@ -606,13 +628,13 @@ if (rlsContext.isSystem()) {
606
628
  // Run as system user (bypass RLS)
607
629
  await rlsContext.asSystemAsync(async () => {
608
630
  // All operations bypass RLS policies
609
- const allPosts = await orm.posts.findAll();
610
- });
631
+ const allPosts = await orm.posts.findAll()
632
+ })
611
633
 
612
634
  // Synchronous system context
613
635
  const result = rlsContext.asSystem(() => {
614
- return someOperation();
615
- });
636
+ return someOperation()
637
+ })
616
638
  ```
617
639
 
618
640
  **Important:** The context helpers (`getContext`, `getAuth`, etc.) throw `RLSContextError` if called outside of a context. Always use `getContextOrNull()` or `hasContext()` if you need to check conditionally.
@@ -630,19 +652,19 @@ Bypass RLS policies for specific operations by running them in a system context:
630
652
  ```typescript
631
653
  // Fetch all posts including other tenants (bypasses RLS)
632
654
  const allPosts = await repo.withoutRLS(async () => {
633
- return repo.findAll();
634
- });
655
+ return repo.findAll()
656
+ })
635
657
 
636
658
  // Compare filtered vs unfiltered results
637
659
  await rlsContext.runAsync(userContext, async () => {
638
- const userPosts = await repo.findAll(); // Filtered by RLS policies
660
+ const userPosts = await repo.findAll() // Filtered by RLS policies
639
661
 
640
662
  const allPosts = await repo.withoutRLS(async () => {
641
- return repo.findAll(); // Bypasses RLS, returns all records
642
- });
663
+ return repo.findAll() // Bypasses RLS, returns all records
664
+ })
643
665
 
644
- console.log(`User can see ${userPosts.length} of ${allPosts.length} total posts`);
645
- });
666
+ console.log(`User can see ${userPosts.length} of ${allPosts.length} total posts`)
667
+ })
646
668
  ```
647
669
 
648
670
  **Implementation:** `withoutRLS` internally calls `rlsContext.asSystemAsync(fn)`, which sets `auth.isSystem = true` for the duration of the callback.
@@ -652,35 +674,36 @@ await rlsContext.runAsync(userContext, async () => {
652
674
  Check if the current user can perform an operation on a specific row:
653
675
 
654
676
  ```typescript
655
- const post = await repo.findById(postId);
677
+ const post = await repo.findById(postId)
656
678
 
657
679
  // Check read access
658
- const canRead = await repo.canAccess('read', post);
680
+ const canRead = await repo.canAccess('read', post)
659
681
 
660
682
  // Check update access before showing edit UI
661
- const canUpdate = await repo.canAccess('update', post);
683
+ const canUpdate = await repo.canAccess('update', post)
662
684
  if (canUpdate) {
663
685
  // Show edit button in UI
664
686
  }
665
687
 
666
688
  // Pre-flight check to avoid policy violations
667
689
  if (await repo.canAccess('delete', post)) {
668
- await repo.delete(post.id);
690
+ await repo.delete(post.id)
669
691
  } else {
670
- console.log('User cannot delete this post');
692
+ console.log('User cannot delete this post')
671
693
  }
672
694
 
673
695
  // Check multiple operations
674
- const operations = ['read', 'update', 'delete'] as const;
696
+ const operations = ['read', 'update', 'delete'] as const
675
697
  for (const op of operations) {
676
- const allowed = await repo.canAccess(op, post);
677
- console.log(`${op}: ${allowed}`);
698
+ const allowed = await repo.canAccess(op, post)
699
+ console.log(`${op}: ${allowed}`)
678
700
  }
679
701
  ```
680
702
 
681
703
  **Implementation:** `canAccess` evaluates the RLS policies against the provided row using the `MutationGuard`, returning `true` if allowed and `false` if denied or no context exists.
682
704
 
683
705
  **Supported Operations:**
706
+
684
707
  - `'read'` - Check if user can view the row
685
708
  - `'create'` - Check if user can create with this data
686
709
  - `'update'` - Check if user can update the row
@@ -690,51 +713,45 @@ for (const op of operations) {
690
713
 
691
714
  ## DAL Pattern Support
692
715
 
693
- RLS works seamlessly with the DAL pattern:
716
+ RLS works seamlessly with the DAL pattern through @kysera/executor's Unified Execution Layer:
694
717
 
695
718
  ```typescript
696
- import { createExecutor } from '@kysera/executor';
697
- import { createContext, createQuery, withTransaction } from '@kysera/dal';
698
- import { rlsPlugin, defineRLSSchema, filter, rlsContext } from '@kysera/rls';
719
+ import { createExecutor } from '@kysera/executor'
720
+ import { createContext, createQuery, withTransaction } from '@kysera/dal'
721
+ import { rlsPlugin, defineRLSSchema, filter, rlsContext } from '@kysera/rls'
699
722
 
700
723
  // Define schema
701
724
  const rlsSchema = defineRLSSchema<Database>({
702
725
  posts: {
703
- policies: [
704
- filter('read', ctx => ({ tenant_id: ctx.auth.tenantId })),
705
- ],
706
- },
707
- });
726
+ policies: [filter('read', ctx => ({ tenant_id: ctx.auth.tenantId }))]
727
+ }
728
+ })
708
729
 
709
- // Create executor with RLS
710
- const executor = await createExecutor(db, [
711
- rlsPlugin({ schema: rlsSchema }),
712
- ]);
730
+ // Step 1: Register RLS plugin with Unified Execution Layer
731
+ const executor = await createExecutor(db, [rlsPlugin({ schema: rlsSchema })])
713
732
 
714
- // Create DAL context
715
- const dalCtx = createContext(executor);
733
+ // Step 2: Create DAL context - plugins automatically apply
734
+ const dalCtx = createContext(executor)
716
735
 
717
- // Define queries - RLS applied automatically
718
- const getPosts = createQuery((ctx) =>
719
- ctx.db.selectFrom('posts').selectAll().execute()
720
- );
736
+ // Step 3: Define queries - RLS applied automatically
737
+ const getPosts = createQuery(ctx => ctx.db.selectFrom('posts').selectAll().execute())
721
738
 
722
739
  // Execute within RLS context
723
740
  await rlsContext.runAsync(
724
741
  {
725
742
  auth: { userId: 1, tenantId: 'acme', roles: ['user'], isSystem: false },
726
- timestamp: new Date(),
743
+ timestamp: new Date()
727
744
  },
728
745
  async () => {
729
746
  // Automatically filtered by tenant
730
- const posts = await getPosts(dalCtx);
747
+ const posts = await getPosts(dalCtx)
731
748
 
732
749
  // Transactions propagate RLS context
733
- await withTransaction(dalCtx, async (txCtx) => {
734
- const txPosts = await getPosts(txCtx);
735
- });
750
+ await withTransaction(dalCtx, async txCtx => {
751
+ const txPosts = await getPosts(txCtx)
752
+ })
736
753
  }
737
- );
754
+ )
738
755
  ```
739
756
 
740
757
  ---
@@ -746,33 +763,62 @@ await rlsContext.runAsync(
746
763
  ```typescript
747
764
  interface RLSPluginOptions<DB = unknown> {
748
765
  /** RLS policy schema (required) */
749
- schema: RLSSchema<DB>;
766
+ schema: RLSSchema<DB>
767
+
768
+ /**
769
+ * Tables to exclude from RLS (always bypass)
770
+ * Replaces deprecated skipTables
771
+ */
772
+ excludeTables?: string[]
750
773
 
751
- /** Tables to skip RLS (always bypass) */
752
- skipTables?: string[];
774
+ /** @deprecated Use excludeTables instead */
775
+ skipTables?: string[]
753
776
 
754
777
  /** Roles that bypass RLS entirely */
755
- bypassRoles?: string[];
778
+ bypassRoles?: string[]
756
779
 
757
780
  /** Logger for RLS operations */
758
- logger?: KyseraLogger;
759
-
760
- /** Require RLS context (throws if missing) */
761
- requireContext?: boolean;
781
+ logger?: KyseraLogger
782
+
783
+ /**
784
+ * Require RLS context (throws if missing)
785
+ * @default true - SECURE BY DEFAULT
786
+ *
787
+ * SECURITY: Changed to true in v0.7.3+ for secure-by-default.
788
+ * When true, missing context throws RLSContextError.
789
+ * Only set to false if you have other security controls.
790
+ */
791
+ requireContext?: boolean
792
+
793
+ /**
794
+ * Allow unfiltered queries when context is missing
795
+ * @default false - SECURE BY DEFAULT
796
+ *
797
+ * WARNING: Setting true allows queries without RLS filtering
798
+ * when context is missing. Only enable if you understand the
799
+ * security implications (e.g., background jobs, system ops).
800
+ *
801
+ * When requireContext=false and allowUnfilteredQueries=false:
802
+ * - Missing context returns empty results with warnings
803
+ */
804
+ allowUnfilteredQueries?: boolean
762
805
 
763
806
  /** Enable audit logging of decisions */
764
- auditDecisions?: boolean;
807
+ auditDecisions?: boolean
765
808
 
766
809
  /** Custom violation handler */
767
- onViolation?: (violation: RLSPolicyViolation) => void;
810
+ onViolation?: (violation: RLSPolicyViolation) => void
811
+
812
+ /** Primary key column name (default: 'id') */
813
+ primaryKeyColumn?: string
768
814
  }
769
815
  ```
770
816
 
771
817
  **Example:**
772
818
 
773
819
  ```typescript
774
- import { rlsPlugin } from '@kysera/rls';
775
- import { createLogger } from '@kysera/core';
820
+ import { rlsPlugin } from '@kysera/rls'
821
+ import { createLogger } from '@kysera/core'
776
822
 
777
823
  const plugin = rlsPlugin({
778
824
  schema: rlsSchema,
@@ -781,19 +827,80 @@ const plugin = rlsPlugin({
781
827
  logger: createLogger({ level: 'info' }),
782
828
  requireContext: true,
783
829
  auditDecisions: true,
784
- onViolation: (violation) => {
830
+ onViolation: violation => {
785
831
  auditLog.record({
786
832
  type: 'rls_violation',
787
833
  operation: violation.operation,
788
834
  table: violation.table,
789
- timestamp: new Date(),
790
- });
791
- },
792
- });
835
+ timestamp: new Date()
836
+ })
837
+ }
838
+ })
793
839
 
794
- const orm = await createORM(db, [plugin]);
840
+ const orm = await createORM(db, [plugin])
795
841
  ```
796
842
 
843
+ ### Security Configuration (v0.7.3+)
844
+
845
+ **BREAKING CHANGE**: Starting in v0.7.3, `requireContext` defaults to `true` for secure-by-default behavior.
846
+
847
+ #### Secure Defaults (Recommended)
848
+
849
+ ```typescript
850
+ // Default behavior - secure by default
851
+ const plugin = rlsPlugin({
852
+ schema: rlsSchema
853
+ // requireContext: true (implicit)
854
+ // allowUnfilteredQueries: false (implicit)
855
+ })
856
+
857
+ // Missing context throws RLSContextError
858
+ await orm.posts.findAll() // ❌ Throws: RLS context required
859
+ ```
860
+
861
+ #### Background Jobs / System Operations
862
+
863
+ For operations that legitimately run without user context (e.g., cron jobs, system maintenance):
864
+
865
+ ```typescript
866
+ const plugin = rlsPlugin({
867
+ schema: rlsSchema,
868
+ requireContext: false, // Don't throw on missing context
869
+ allowUnfilteredQueries: true // Allow queries without filtering
870
+ })
871
+
872
+ // OR use system context for privileged operations:
873
+ await rlsContext.asSystemAsync(async () => {
874
+ await orm.posts.findAll() // ✅ Runs as system user
875
+ })
876
+ ```
877
+
878
+ #### Defensive Mode (No throws, but safe)
879
+
880
+ For applications transitioning to RLS or with mixed code paths:
881
+
882
+ ```typescript
883
+ const plugin = rlsPlugin({
884
+ schema: rlsSchema,
885
+ requireContext: false, // Don't throw
886
+ allowUnfilteredQueries: false // Return empty results
887
+ // Missing context logs warning and returns no rows
888
+ })
889
+ ```
890
+
891
+ **Security Matrix:**
892
+
893
+ | requireContext | allowUnfilteredQueries | Missing Context Behavior |
894
+ | -------------- | ---------------------- | --------------------------------------- |
895
+ | `true` (default) | N/A | **Throws RLSContextError** (secure) |
896
+ | `false` | `false` (default) | **Returns empty results** (safe) |
897
+ | `false` | `true` | **Allows unfiltered access** (unsafe) |
898
+
899
+ ⚠️ **Security Warning**: Only use `allowUnfilteredQueries: true` if you:
900
+ 1. Understand the security implications
901
+ 2. Have other security controls in place
902
+ 3. Are running background jobs or system operations without user context
903
+
797
904
  ---
798
905
 
799
906
  ## Error Handling
@@ -804,13 +911,13 @@ The RLS plugin provides specialized error classes for different failure scenario
804
911
 
805
912
  ```typescript
806
913
  import {
807
- RLSError, // Base error class
808
- RLSContextError, // Missing context
809
- RLSPolicyViolation, // Access denied (expected)
810
- RLSPolicyEvaluationError, // Bug in policy code (unexpected)
811
- RLSSchemaError, // Invalid schema
812
- RLSContextValidationError, // Invalid context
813
- } from '@kysera/rls';
914
+ RLSError, // Base error class
915
+ RLSContextError, // Missing context
916
+ RLSPolicyViolation, // Access denied (expected)
917
+ RLSPolicyEvaluationError, // Bug in policy code (unexpected)
918
+ RLSSchemaError, // Invalid schema
919
+ RLSContextValidationError // Invalid context
920
+ } from '@kysera/rls'
814
921
  ```
815
922
 
816
923
  ### Error Scenarios
@@ -822,16 +929,17 @@ Thrown when RLS context is missing but required:
822
929
  ```typescript
823
930
  try {
824
931
  // No context set, but requireContext: true
825
- await orm.posts.findAll();
932
+ await orm.posts.findAll()
826
933
  } catch (error) {
827
934
  if (error instanceof RLSContextError) {
828
935
  // error.code === 'RLS_CONTEXT_MISSING'
829
- console.error('No RLS context found. Ensure code runs within rlsContext.runAsync()');
936
+ console.error('No RLS context found. Ensure code runs within rlsContext.runAsync()')
830
937
  }
831
938
  }
832
939
  ```
833
940
 
834
941
  **When thrown:**
942
+
835
943
  - Operations executed outside `rlsContext.runAsync()` when `requireContext: true`
836
944
  - Calling `rlsContext.getContext()` without active context
837
945
  - Attempting `asSystem()` without existing context
@@ -843,27 +951,28 @@ Thrown when operation is denied by policies (this is expected, not a bug):
843
951
  ```typescript
844
952
  try {
845
953
  // User tries to update a post they don't own
846
- await orm.posts.update(1, { title: 'New Title' });
954
+ await orm.posts.update(1, { title: 'New Title' })
847
955
  } catch (error) {
848
956
  if (error instanceof RLSPolicyViolation) {
849
957
  // error.code === 'RLS_POLICY_VIOLATION'
850
958
  console.error({
851
- operation: error.operation, // 'update'
852
- table: error.table, // 'posts'
853
- reason: error.reason, // 'User does not own this post'
854
- policyName: error.policyName, // 'ownership_policy' (if named)
855
- });
959
+ operation: error.operation, // 'update'
960
+ table: error.table, // 'posts'
961
+ reason: error.reason, // 'User does not own this post'
962
+ policyName: error.policyName // 'ownership_policy' (if named)
963
+ })
856
964
 
857
965
  // Return 403 Forbidden to client
858
966
  res.status(403).json({
859
967
  error: 'Access denied',
860
- message: error.reason,
861
- });
968
+ message: error.reason
969
+ })
862
970
  }
863
971
  }
864
972
  ```
865
973
 
866
974
  **When thrown:**
975
+
867
976
  - `deny` policy condition evaluates to `true`
868
977
  - No `allow` policy matches and `defaultDeny: true`
869
978
  - `validate` policy fails during CREATE/UPDATE
@@ -874,16 +983,16 @@ Thrown when policy condition throws an error (this is a bug in your policy code)
874
983
 
875
984
  ```typescript
876
985
  try {
877
- await orm.posts.findAll();
986
+ await orm.posts.findAll()
878
987
  } catch (error) {
879
988
  if (error instanceof RLSPolicyEvaluationError) {
880
989
  // error.code === 'RLS_POLICY_EVALUATION_ERROR'
881
990
  console.error({
882
- operation: error.operation, // 'read'
883
- table: error.table, // 'posts'
884
- policyName: error.policyName, // 'tenant_filter'
885
- originalError: error.originalError, // TypeError: Cannot read property 'tenantId' of undefined
886
- });
991
+ operation: error.operation, // 'read'
992
+ table: error.table, // 'posts'
993
+ policyName: error.policyName, // 'tenant_filter'
994
+ originalError: error.originalError // TypeError: Cannot read property 'tenantId' of undefined
995
+ })
887
996
 
888
997
  // This is a bug - fix your policy code!
889
998
  // Example: Policy tried to access ctx.auth.tenantId but it was undefined
@@ -892,6 +1001,7 @@ try {
892
1001
  ```
893
1002
 
894
1003
  **When thrown:**
1004
+
895
1005
  - Policy condition function throws an error
896
1006
  - Policy tries to access undefined properties
897
1007
  - Async policy rejects with an error
@@ -907,16 +1017,16 @@ try {
907
1017
  const ctx = createRLSContext({
908
1018
  auth: {
909
1019
  // Missing userId!
910
- roles: ['user'],
911
- },
912
- });
1020
+ roles: ['user']
1021
+ }
1022
+ })
913
1023
  } catch (error) {
914
1024
  if (error instanceof RLSContextValidationError) {
915
1025
  // error.code === 'RLS_CONTEXT_INVALID'
916
1026
  console.error({
917
- message: error.message, // 'userId is required in auth context'
918
- field: error.field, // 'userId'
919
- });
1027
+ message: error.message, // 'userId is required in auth context'
1028
+ field: error.field // 'userId'
1029
+ })
920
1030
  }
921
1031
  }
922
1032
  ```
@@ -934,31 +1044,31 @@ try {
934
1044
  { type: 'invalid-type', operation: 'read', condition: () => true }
935
1045
  ]
936
1046
  }
937
- });
1047
+ })
938
1048
  } catch (error) {
939
1049
  if (error instanceof RLSSchemaError) {
940
1050
  // error.code === 'RLS_SCHEMA_INVALID'
941
- console.error(error.details);
1051
+ console.error(error.details)
942
1052
  }
943
1053
  }
944
1054
  ```
945
1055
 
946
1056
  ### Error Comparison Table
947
1057
 
948
- | Error | Meaning | Severity | Action |
949
- |-------|---------|----------|--------|
950
- | `RLSContextError` | Missing context | Error | Ensure code runs in `rlsContext.runAsync()` |
951
- | `RLSPolicyViolation` | Access denied | Expected | Return 403 to client, normal behavior |
952
- | `RLSPolicyEvaluationError` | Policy bug | Critical | Fix the policy code immediately |
953
- | `RLSContextValidationError` | Invalid context | Error | Fix context creation |
954
- | `RLSSchemaError` | Invalid schema | Error | Fix schema definition |
1058
+ | Error | Meaning | Severity | Action |
1059
+ | --------------------------- | --------------- | -------- | ------------------------------------------- |
1060
+ | `RLSContextError` | Missing context | Error | Ensure code runs in `rlsContext.runAsync()` |
1061
+ | `RLSPolicyViolation` | Access denied | Expected | Return 403 to client, normal behavior |
1062
+ | `RLSPolicyEvaluationError` | Policy bug | Critical | Fix the policy code immediately |
1063
+ | `RLSContextValidationError` | Invalid context | Error | Fix context creation |
1064
+ | `RLSSchemaError` | Invalid schema | Error | Fix schema definition |
955
1065
 
956
1066
  ### Error Codes
957
1067
 
958
1068
  All RLS errors include a `code` property for programmatic handling:
959
1069
 
960
1070
  ```typescript
961
- import { RLSErrorCodes } from '@kysera/rls';
1071
+ import { RLSErrorCodes } from '@kysera/rls'
962
1072
 
963
1073
  // RLSErrorCodes.RLS_CONTEXT_MISSING
964
1074
  // RLSErrorCodes.RLS_POLICY_VIOLATION
@@ -997,21 +1107,25 @@ The RLS plugin follows the standard `@kysera/executor` plugin lifecycle:
997
1107
  ### Key Components
998
1108
 
999
1109
  **PolicyRegistry:**
1110
+
1000
1111
  - Stores and indexes compiled policies by table and operation
1001
1112
  - Validates schema structure
1002
1113
  - Provides fast policy lookup
1003
1114
 
1004
1115
  **SelectTransformer:**
1116
+
1005
1117
  - Transforms SELECT queries by adding WHERE conditions
1006
1118
  - Combines multiple filter policies with AND logic
1007
1119
  - Evaluates filter conditions in context
1008
1120
 
1009
1121
  **MutationGuard:**
1122
+
1010
1123
  - Evaluates allow/deny policies for mutations
1011
1124
  - Enforces policy evaluation order (deny → allow → validate)
1012
1125
  - Throws `RLSPolicyViolation` or `RLSPolicyEvaluationError`
1013
1126
 
1014
1127
  **AsyncLocalStorage:**
1128
+
1015
1129
  - Provides context isolation per request
1016
1130
  - Automatic propagation through async/await chains
1017
1131
  - No manual context passing required
@@ -1019,19 +1133,23 @@ The RLS plugin follows the standard `@kysera/executor` plugin lifecycle:
1019
1133
  ### Performance Considerations
1020
1134
 
1021
1135
  **Compiled Policies:**
1136
+
1022
1137
  - Policies are compiled once at initialization
1023
1138
  - No runtime parsing or compilation overhead
1024
1139
 
1025
1140
  **Filter Application:**
1141
+
1026
1142
  - Filters applied as SQL WHERE clauses
1027
1143
  - Database handles filtering efficiently
1028
1144
  - Index hints available via `PolicyOptions.hints`
1029
1145
 
1030
1146
  **Context Access:**
1147
+
1031
1148
  - AsyncLocalStorage is very fast (V8-optimized)
1032
1149
  - Context lookup has negligible overhead
1033
1150
 
1034
1151
  **Bypass Mechanisms:**
1152
+
1035
1153
  - System context bypass is immediate (no policy evaluation)
1036
1154
  - `skipTables` bypass is immediate (no policy evaluation)
1037
1155
  - Bypass roles checked before policy evaluation
@@ -1043,21 +1161,21 @@ RLS context automatically propagates through transactions:
1043
1161
  ```typescript
1044
1162
  await rlsContext.runAsync(userContext, async () => {
1045
1163
  // Context available in transaction
1046
- await orm.transaction(async (trx) => {
1164
+ await orm.transaction(async trx => {
1047
1165
  // All queries use the same RLS context
1048
- const user = await trx.users.findById(userId);
1049
- await trx.posts.create({ title: 'Post', authorId: userId });
1050
- });
1051
- });
1166
+ const user = await trx.users.findById(userId)
1167
+ await trx.posts.create({ title: 'Post', authorId: userId })
1168
+ })
1169
+ })
1052
1170
  ```
1053
1171
 
1054
1172
  **Note:** DAL transactions with executor preserve RLS context:
1055
1173
 
1056
1174
  ```typescript
1057
- await withTransaction(executor, async (txCtx) => {
1175
+ await withTransaction(executor, async txCtx => {
1058
1176
  // RLS context preserved in transaction
1059
- const posts = await getPosts(txCtx);
1060
- });
1177
+ const posts = await getPosts(txCtx)
1178
+ })
1061
1179
  ```
1062
1180
 
1063
1181
  ---
@@ -1071,23 +1189,23 @@ const schema = defineRLSSchema<Database>({
1071
1189
  posts: {
1072
1190
  policies: [
1073
1191
  filter('read', ctx => ({ tenant_id: ctx.auth.tenantId })),
1074
- validate('create', ctx => ctx.data.tenant_id === ctx.auth.tenantId),
1192
+ validate('create', ctx => ctx.data.tenant_id === ctx.auth.tenantId)
1075
1193
  ],
1076
- defaultDeny: true,
1077
- },
1078
- });
1194
+ defaultDeny: true
1195
+ }
1196
+ })
1079
1197
 
1080
1198
  app.use(async (req, res, next) => {
1081
- const user = await authenticate(req);
1199
+ const user = await authenticate(req)
1082
1200
 
1083
1201
  await rlsContext.runAsync(
1084
1202
  { auth: { userId: user.id, tenantId: user.tenant_id, roles: user.roles } },
1085
1203
  async () => {
1086
- const posts = await orm.posts.findAll();
1087
- res.json(posts);
1204
+ const posts = await orm.posts.findAll()
1205
+ res.json(posts)
1088
1206
  }
1089
- );
1090
- });
1207
+ )
1208
+ })
1091
1209
  ```
1092
1210
 
1093
1211
  ### Owner-Based Access
@@ -1103,12 +1221,10 @@ const schema = defineRLSSchema<Database>({
1103
1221
  allow('read', ctx => ctx.auth.userId === ctx.row.author_id),
1104
1222
 
1105
1223
  // Only owner can update/delete
1106
- allow(['update', 'delete'], ctx =>
1107
- ctx.auth.userId === ctx.row.author_id
1108
- ),
1109
- ],
1110
- },
1111
- });
1224
+ allow(['update', 'delete'], ctx => ctx.auth.userId === ctx.row.author_id)
1225
+ ]
1226
+ }
1227
+ })
1112
1228
  ```
1113
1229
 
1114
1230
  ### Role-Based Access Control
@@ -1121,15 +1237,13 @@ const schema = defineRLSSchema<Database>({
1121
1237
  allow('all', ctx => ctx.auth.roles.includes('admin')),
1122
1238
 
1123
1239
  // Editors can read and update
1124
- allow(['read', 'update'], ctx =>
1125
- ctx.auth.roles.includes('editor')
1126
- ),
1240
+ allow(['read', 'update'], ctx => ctx.auth.roles.includes('editor')),
1127
1241
 
1128
1242
  // Regular users read only
1129
- allow('read', ctx => ctx.auth.roles.includes('user')),
1130
- ],
1131
- },
1132
- });
1243
+ allow('read', ctx => ctx.auth.roles.includes('user'))
1244
+ ]
1245
+ }
1246
+ })
1133
1247
  ```
1134
1248
 
1135
1249
  ---
@@ -1141,30 +1255,30 @@ Full type inference for policies:
1141
1255
  ```typescript
1142
1256
  interface Database {
1143
1257
  posts: {
1144
- id: number;
1145
- title: string;
1146
- author_id: number;
1147
- tenant_id: string;
1148
- };
1258
+ id: number
1259
+ title: string
1260
+ author_id: number
1261
+ tenant_id: string
1262
+ }
1149
1263
  }
1150
1264
 
1151
1265
  const schema = defineRLSSchema<Database>({
1152
1266
  posts: {
1153
1267
  policies: [
1154
1268
  allow('read', ctx => {
1155
- const post = ctx.row; // Type: Database['posts']
1156
- const userId = ctx.auth.userId; // Type: string | number
1157
- return post.author_id === userId;
1269
+ const post = ctx.row // Type: Database['posts']
1270
+ const userId = ctx.auth.userId // Type: string | number
1271
+ return post.author_id === userId
1158
1272
  }),
1159
1273
 
1160
1274
  validate('update', ctx => {
1161
- const data = ctx.data; // Type: Partial<Database['posts']>
1162
- const title = data.title; // Type: string | undefined
1163
- return !title || title.length > 0;
1164
- }),
1165
- ],
1166
- },
1167
- });
1275
+ const data = ctx.data // Type: Partial<Database['posts']>
1276
+ const title = data.title // Type: string | undefined
1277
+ return !title || title.length > 0
1278
+ })
1279
+ ]
1280
+ }
1281
+ })
1168
1282
  ```
1169
1283
 
1170
1284
  ---
@@ -1175,13 +1289,13 @@ const schema = defineRLSSchema<Database>({
1175
1289
 
1176
1290
  ```typescript
1177
1291
  // Schema definition
1178
- export { defineRLSSchema, mergeRLSSchemas } from '@kysera/rls';
1292
+ export { defineRLSSchema, mergeRLSSchemas } from '@kysera/rls'
1179
1293
 
1180
1294
  // Policy builders
1181
- export { allow, deny, filter, validate, type PolicyOptions } from '@kysera/rls';
1295
+ export { allow, deny, filter, validate, type PolicyOptions } from '@kysera/rls'
1182
1296
 
1183
1297
  // Plugin
1184
- export { rlsPlugin, type RLSPluginOptions } from '@kysera/rls';
1298
+ export { rlsPlugin, type RLSPluginOptions } from '@kysera/rls'
1185
1299
 
1186
1300
  // Context management
1187
1301
  export {
@@ -1189,8 +1303,8 @@ export {
1189
1303
  createRLSContext,
1190
1304
  withRLSContext,
1191
1305
  withRLSContextAsync,
1192
- type RLSContext,
1193
- } from '@kysera/rls';
1306
+ type RLSContext
1307
+ } from '@kysera/rls'
1194
1308
 
1195
1309
  // Errors
1196
1310
  export {
@@ -1200,8 +1314,8 @@ export {
1200
1314
  RLSPolicyEvaluationError,
1201
1315
  RLSSchemaError,
1202
1316
  RLSContextValidationError,
1203
- RLSErrorCodes,
1204
- } from '@kysera/rls';
1317
+ RLSErrorCodes
1318
+ } from '@kysera/rls'
1205
1319
  ```
1206
1320
 
1207
1321
  ---