@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 +390 -276
- package/dist/index.d.ts +95 -19
- package/dist/index.js +219 -130
- package/dist/index.js.map +1 -1
- package/dist/native/index.d.ts +1 -1
- package/dist/native/index.js +3 -14
- package/dist/native/index.js.map +1 -1
- package/dist/{types-6eCXh_Jd.d.ts → types-Dowjd6zG.d.ts} +3 -3
- package/package.json +20 -11
- package/src/context/index.ts +4 -4
- package/src/context/manager.ts +45 -45
- package/src/context/storage.ts +3 -3
- package/src/context/types.ts +1 -5
- package/src/errors.ts +62 -77
- package/src/index.ts +13 -13
- package/src/native/README.md +49 -46
- package/src/native/index.ts +3 -6
- package/src/native/migration.ts +29 -27
- package/src/native/postgres.ts +63 -74
- package/src/plugin.ts +306 -159
- package/src/policy/builder.ts +46 -33
- package/src/policy/index.ts +4 -4
- package/src/policy/registry.ts +100 -105
- package/src/policy/schema.ts +58 -71
- package/src/policy/types.ts +58 -58
- package/src/transformer/index.ts +2 -2
- package/src/transformer/mutation.ts +95 -98
- package/src/transformer/select.ts +59 -43
- package/src/utils/helpers.ts +57 -50
- package/src/utils/index.ts +13 -2
- package/src/utils/type-utils.ts +155 -0
- package/src/version.ts +7 -0
package/src/utils/helpers.ts
CHANGED
|
@@ -2,87 +2,96 @@
|
|
|
2
2
|
* Utility helper functions for RLS
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type {
|
|
6
|
-
RLSContext,
|
|
7
|
-
PolicyEvaluationContext,
|
|
8
|
-
Operation,
|
|
9
|
-
} from '../policy/types.js';
|
|
5
|
+
import type { RLSContext, PolicyEvaluationContext, Operation } from '../policy/types.js'
|
|
10
6
|
|
|
11
7
|
/**
|
|
12
8
|
* Create a policy evaluation context from RLS context
|
|
13
9
|
*/
|
|
14
|
-
export function createEvaluationContext<
|
|
15
|
-
TRow = unknown,
|
|
16
|
-
TData = unknown
|
|
17
|
-
>(
|
|
10
|
+
export function createEvaluationContext<TRow = unknown, TData = unknown>(
|
|
18
11
|
rlsCtx: RLSContext,
|
|
19
12
|
options?: {
|
|
20
|
-
row?: TRow
|
|
21
|
-
data?: TData
|
|
13
|
+
row?: TRow
|
|
14
|
+
data?: TData
|
|
22
15
|
}
|
|
23
16
|
): PolicyEvaluationContext<unknown, TRow, TData> {
|
|
24
17
|
const ctx: PolicyEvaluationContext<unknown, TRow, TData> = {
|
|
25
|
-
auth: rlsCtx.auth
|
|
26
|
-
}
|
|
18
|
+
auth: rlsCtx.auth
|
|
19
|
+
}
|
|
27
20
|
|
|
28
21
|
if (options?.row !== undefined) {
|
|
29
|
-
ctx.row = options.row
|
|
22
|
+
ctx.row = options.row
|
|
30
23
|
}
|
|
31
24
|
|
|
32
25
|
if (options?.data !== undefined) {
|
|
33
|
-
ctx.data = options.data
|
|
26
|
+
ctx.data = options.data
|
|
34
27
|
}
|
|
35
28
|
|
|
36
29
|
if (rlsCtx.request !== undefined) {
|
|
37
|
-
ctx.request = rlsCtx.request
|
|
30
|
+
ctx.request = rlsCtx.request
|
|
38
31
|
}
|
|
39
32
|
|
|
40
33
|
if (rlsCtx.meta !== undefined) {
|
|
41
|
-
ctx.meta = rlsCtx.meta as Record<string, unknown
|
|
34
|
+
ctx.meta = rlsCtx.meta as Record<string, unknown>
|
|
42
35
|
}
|
|
43
36
|
|
|
44
|
-
return ctx
|
|
37
|
+
return ctx
|
|
45
38
|
}
|
|
46
39
|
|
|
47
40
|
/**
|
|
48
41
|
* Check if a condition function is async
|
|
42
|
+
*
|
|
43
|
+
* NOTE: This function checks both constructor.name (for native async functions)
|
|
44
|
+
* and return type (for transpiled code that returns Promise).
|
|
45
|
+
* Transpilers often convert async functions to regular functions that return Promise.
|
|
49
46
|
*/
|
|
50
47
|
export function isAsyncFunction(fn: unknown): fn is (...args: unknown[]) => Promise<unknown> {
|
|
51
|
-
|
|
48
|
+
if (!(fn instanceof Function)) {
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check constructor name for native async functions
|
|
53
|
+
if (fn.constructor.name === 'AsyncFunction') {
|
|
54
|
+
return true
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// For transpiled code: call the function with empty args and check if it returns a Promise
|
|
58
|
+
// This is safe because policy conditions should be pure functions
|
|
59
|
+
try {
|
|
60
|
+
const result = (fn as Function)()
|
|
61
|
+
return result instanceof Promise
|
|
62
|
+
} catch {
|
|
63
|
+
// If calling with no args throws, assume it's not async
|
|
64
|
+
// (async functions that require args should be wrapped in the policy definition)
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
52
67
|
}
|
|
53
68
|
|
|
54
69
|
/**
|
|
55
70
|
* Safely evaluate a policy condition
|
|
56
71
|
*/
|
|
57
|
-
export async function safeEvaluate<T>(
|
|
58
|
-
fn: () => T | Promise<T>,
|
|
59
|
-
defaultValue: T
|
|
60
|
-
): Promise<T> {
|
|
72
|
+
export async function safeEvaluate<T>(fn: () => T | Promise<T>, defaultValue: T): Promise<T> {
|
|
61
73
|
try {
|
|
62
|
-
const result = fn()
|
|
74
|
+
const result = fn()
|
|
63
75
|
if (result instanceof Promise) {
|
|
64
|
-
return await result
|
|
76
|
+
return await result
|
|
65
77
|
}
|
|
66
|
-
return result
|
|
67
|
-
} catch (
|
|
78
|
+
return result
|
|
79
|
+
} catch (_error) {
|
|
68
80
|
// Expected failure during policy evaluation - return default value
|
|
69
81
|
// Logger not available in this utility function, error is handled gracefully
|
|
70
|
-
return defaultValue
|
|
82
|
+
return defaultValue
|
|
71
83
|
}
|
|
72
84
|
}
|
|
73
85
|
|
|
74
86
|
/**
|
|
75
87
|
* Deep merge two objects
|
|
76
88
|
*/
|
|
77
|
-
export function deepMerge<T extends Record<string, unknown>>(
|
|
78
|
-
target
|
|
79
|
-
source: Partial<T>
|
|
80
|
-
): T {
|
|
81
|
-
const result = { ...target };
|
|
89
|
+
export function deepMerge<T extends Record<string, unknown>>(target: T, source: Partial<T>): T {
|
|
90
|
+
const result = { ...target }
|
|
82
91
|
|
|
83
92
|
for (const key of Object.keys(source) as (keyof T)[]) {
|
|
84
|
-
const sourceValue = source[key]
|
|
85
|
-
const targetValue = result[key]
|
|
93
|
+
const sourceValue = source[key]
|
|
94
|
+
const targetValue = result[key]
|
|
86
95
|
|
|
87
96
|
if (
|
|
88
97
|
sourceValue !== undefined &&
|
|
@@ -96,44 +105,42 @@ export function deepMerge<T extends Record<string, unknown>>(
|
|
|
96
105
|
result[key] = deepMerge(
|
|
97
106
|
targetValue as Record<string, unknown>,
|
|
98
107
|
sourceValue as Record<string, unknown>
|
|
99
|
-
) as T[keyof T]
|
|
108
|
+
) as T[keyof T]
|
|
100
109
|
} else if (sourceValue !== undefined) {
|
|
101
|
-
result[key] = sourceValue as T[keyof T]
|
|
110
|
+
result[key] = sourceValue as T[keyof T]
|
|
102
111
|
}
|
|
103
112
|
}
|
|
104
113
|
|
|
105
|
-
return result
|
|
114
|
+
return result
|
|
106
115
|
}
|
|
107
116
|
|
|
108
117
|
/**
|
|
109
118
|
* Create a simple hash for cache keys
|
|
110
119
|
*/
|
|
111
120
|
export function hashString(str: string): string {
|
|
112
|
-
let hash = 0
|
|
121
|
+
let hash = 0
|
|
113
122
|
for (let i = 0; i < str.length; i++) {
|
|
114
|
-
const char = str.charCodeAt(i)
|
|
115
|
-
hash = (
|
|
116
|
-
hash = hash & hash
|
|
123
|
+
const char = str.charCodeAt(i)
|
|
124
|
+
hash = (hash << 5) - hash + char
|
|
125
|
+
hash = hash & hash // Convert to 32bit integer
|
|
117
126
|
}
|
|
118
|
-
return hash.toString(36)
|
|
127
|
+
return hash.toString(36)
|
|
119
128
|
}
|
|
120
129
|
|
|
121
130
|
/**
|
|
122
131
|
* Normalize operations to array format
|
|
123
132
|
*/
|
|
124
|
-
export function normalizeOperations(
|
|
125
|
-
operation: Operation | Operation[]
|
|
126
|
-
): Operation[] {
|
|
133
|
+
export function normalizeOperations(operation: Operation | Operation[]): Operation[] {
|
|
127
134
|
if (Array.isArray(operation)) {
|
|
128
135
|
if (operation.includes('all')) {
|
|
129
|
-
return ['read', 'create', 'update', 'delete']
|
|
136
|
+
return ['read', 'create', 'update', 'delete']
|
|
130
137
|
}
|
|
131
|
-
return operation
|
|
138
|
+
return operation
|
|
132
139
|
}
|
|
133
140
|
|
|
134
141
|
if (operation === 'all') {
|
|
135
|
-
return ['read', 'create', 'update', 'delete']
|
|
142
|
+
return ['read', 'create', 'update', 'delete']
|
|
136
143
|
}
|
|
137
144
|
|
|
138
|
-
return [operation]
|
|
145
|
+
return [operation]
|
|
139
146
|
}
|
package/src/utils/index.ts
CHANGED
|
@@ -8,5 +8,16 @@ export {
|
|
|
8
8
|
safeEvaluate,
|
|
9
9
|
deepMerge,
|
|
10
10
|
hashString,
|
|
11
|
-
normalizeOperations
|
|
12
|
-
} from './helpers.js'
|
|
11
|
+
normalizeOperations
|
|
12
|
+
} from './helpers.js'
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
createQualifiedColumn,
|
|
16
|
+
applyWhereCondition,
|
|
17
|
+
createRawCondition,
|
|
18
|
+
selectFromDynamicTable,
|
|
19
|
+
whereIdEquals,
|
|
20
|
+
transformQueryBuilder,
|
|
21
|
+
hasRawDb,
|
|
22
|
+
getRawDbSafe
|
|
23
|
+
} from './type-utils.js'
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type utilities for RLS plugin
|
|
3
|
+
*
|
|
4
|
+
* These utilities provide type-safe wrappers around dynamic operations
|
|
5
|
+
* that require runtime flexibility beyond TypeScript's compile-time constraints.
|
|
6
|
+
*
|
|
7
|
+
* NOTE: This file intentionally uses `any` types to bridge the gap between
|
|
8
|
+
* Kysely's compile-time type system and RLS's runtime dynamic requirements.
|
|
9
|
+
* All `any` usage is documented and justified with runtime safety guarantees.
|
|
10
|
+
*
|
|
11
|
+
* @module @kysera/rls/utils/type-utils
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
15
|
+
|
|
16
|
+
import type { SelectQueryBuilder, Kysely, RawBuilder } from 'kysely'
|
|
17
|
+
import { sql } from 'kysely'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Type-safe wrapper for dynamic column references in WHERE clauses
|
|
21
|
+
*
|
|
22
|
+
* Kysely's type system requires compile-time known column names, but RLS policies
|
|
23
|
+
* work with dynamic column names at runtime. This utility provides a type-safe
|
|
24
|
+
* boundary for this conversion.
|
|
25
|
+
*
|
|
26
|
+
* Type safety is maintained through:
|
|
27
|
+
* 1. Column names come from validated policy definitions (developer-controlled)
|
|
28
|
+
* 2. Values are type-checked by policy condition functions
|
|
29
|
+
* 3. Runtime validation during policy registration
|
|
30
|
+
*
|
|
31
|
+
* @param table - Table name (from validated policy schema)
|
|
32
|
+
* @param column - Column name (from validated policy definition)
|
|
33
|
+
* @returns Type-safe column reference for Kysely query builder
|
|
34
|
+
*/
|
|
35
|
+
export function createQualifiedColumn(table: string, column: string): string {
|
|
36
|
+
return `${table}.${column}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Type-safe wrapper for applying WHERE conditions from RLS filters
|
|
41
|
+
*
|
|
42
|
+
* This function encapsulates the type boundary between runtime policy conditions
|
|
43
|
+
* and Kysely's compile-time type system.
|
|
44
|
+
*
|
|
45
|
+
* @param qb - Query builder to modify
|
|
46
|
+
* @param column - Qualified column name (table.column)
|
|
47
|
+
* @param operator - Comparison operator
|
|
48
|
+
* @param value - Value to compare against
|
|
49
|
+
* @returns Modified query builder
|
|
50
|
+
*/
|
|
51
|
+
export function applyWhereCondition<DB, TB extends keyof DB & string, O>(
|
|
52
|
+
qb: SelectQueryBuilder<DB, TB, O>,
|
|
53
|
+
column: string,
|
|
54
|
+
operator: 'is' | '=' | 'in',
|
|
55
|
+
value: unknown
|
|
56
|
+
): SelectQueryBuilder<DB, TB, O> {
|
|
57
|
+
return qb.where(column as any, operator as any, value as any)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Type-safe wrapper for raw SQL expressions
|
|
62
|
+
*
|
|
63
|
+
* @param expression - SQL expression (e.g., 'FALSE' for impossible conditions)
|
|
64
|
+
* @returns Type-safe raw builder for WHERE clauses
|
|
65
|
+
*/
|
|
66
|
+
export function createRawCondition(expression: string): RawBuilder<boolean> {
|
|
67
|
+
return sql`${sql.raw(expression)}`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Type-safe wrapper for dynamic table queries (used in raw db queries)
|
|
72
|
+
*
|
|
73
|
+
* This is used by plugins to bypass RLS filtering when fetching existing rows
|
|
74
|
+
* for mutation validation. The table name comes from repository configuration
|
|
75
|
+
* and is validated during repository creation.
|
|
76
|
+
*
|
|
77
|
+
* @param db - Kysely database instance
|
|
78
|
+
* @param table - Table name (from repository config)
|
|
79
|
+
* @returns Query builder for the table
|
|
80
|
+
*/
|
|
81
|
+
export function selectFromDynamicTable<DB>(
|
|
82
|
+
db: Kysely<DB>,
|
|
83
|
+
table: string
|
|
84
|
+
): SelectQueryBuilder<Record<string, unknown>, string, Record<string, unknown>> {
|
|
85
|
+
return db.selectFrom(table as any).selectAll() as any
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Add WHERE clause for primary key equality.
|
|
90
|
+
* Supports custom primary key column names.
|
|
91
|
+
*
|
|
92
|
+
* @param qb - Select query builder
|
|
93
|
+
* @param id - Primary key value
|
|
94
|
+
* @param primaryKeyColumn - Primary key column name (default: 'id')
|
|
95
|
+
* @returns Query builder with ID filter
|
|
96
|
+
*/
|
|
97
|
+
export function whereIdEquals(
|
|
98
|
+
qb: SelectQueryBuilder<any, any, any>,
|
|
99
|
+
id: unknown,
|
|
100
|
+
primaryKeyColumn = 'id'
|
|
101
|
+
): SelectQueryBuilder<any, any, any> {
|
|
102
|
+
return qb.where(primaryKeyColumn as any, '=', id as any)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Type-safe wrapper for transforming query builders in plugin interceptors
|
|
107
|
+
*
|
|
108
|
+
* Used when plugins need to transform a generic query builder (QB) to a specific
|
|
109
|
+
* type (e.g., SelectQueryBuilder) and back. This is necessary because the executor's
|
|
110
|
+
* interceptQuery hook receives unconstrained QB types to preserve type inference.
|
|
111
|
+
*
|
|
112
|
+
* @param qb - Generic query builder from interceptor
|
|
113
|
+
* @param operation - Operation type (for runtime validation)
|
|
114
|
+
* @param transform - Transformation function
|
|
115
|
+
* @returns Transformed query builder
|
|
116
|
+
*/
|
|
117
|
+
export function transformQueryBuilder<QB>(
|
|
118
|
+
qb: QB,
|
|
119
|
+
operation: string,
|
|
120
|
+
transform: (
|
|
121
|
+
qb: SelectQueryBuilder<Record<string, unknown>, string, Record<string, unknown>>
|
|
122
|
+
) => SelectQueryBuilder<Record<string, unknown>, string, Record<string, unknown>>
|
|
123
|
+
): QB {
|
|
124
|
+
if (operation !== 'select') {
|
|
125
|
+
return qb
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const transformed = transform(qb as any)
|
|
129
|
+
return transformed as QB
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Type guard to check if an executor has a raw db instance
|
|
134
|
+
*
|
|
135
|
+
* @param executor - Kysely executor (may have __rawDb property)
|
|
136
|
+
* @returns True if executor has __rawDb property
|
|
137
|
+
*/
|
|
138
|
+
export function hasRawDb<DB>(
|
|
139
|
+
executor: Kysely<DB>
|
|
140
|
+
): executor is Kysely<DB> & { __rawDb: Kysely<DB> } {
|
|
141
|
+
return '__rawDb' in executor && (executor as any).__rawDb !== undefined
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Type-safe wrapper to get raw db from executor
|
|
146
|
+
*
|
|
147
|
+
* @param executor - Kysely executor with optional __rawDb
|
|
148
|
+
* @returns Raw db instance or original executor
|
|
149
|
+
*/
|
|
150
|
+
export function getRawDbSafe<DB>(executor: Kysely<DB>): Kysely<DB> {
|
|
151
|
+
if (hasRawDb(executor)) {
|
|
152
|
+
return executor.__rawDb
|
|
153
|
+
}
|
|
154
|
+
return executor
|
|
155
|
+
}
|
package/src/version.ts
ADDED