@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.
@@ -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
- return fn instanceof Function && fn.constructor.name === 'AsyncFunction';
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 (error) {
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: T,
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 = ((hash << 5) - hash) + char;
116
- hash = hash & hash; // Convert to 32bit integer
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
  }
@@ -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
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Package version - injected at build time by tsup
3
+ * Falls back to development version if not replaced
4
+ * @internal
5
+ */
6
+ const RAW_VERSION = '__VERSION__'
7
+ export const VERSION = RAW_VERSION.startsWith('__') ? '0.0.0-dev' : RAW_VERSION