import type { RLSSchema } from '../policy/types.js' import { PostgresRLSGenerator, type PostgresRLSOptions } from './postgres.js' /** * Options for migration generation */ export interface MigrationOptions extends PostgresRLSOptions { /** Migration name */ name?: string /** Include context functions in migration */ includeContextFunctions?: boolean } /** * RLS Migration Generator * Generates Kysely migration files for RLS policies */ export class RLSMigrationGenerator { private generator = new PostgresRLSGenerator() /** * Generate migration file content */ generateMigration(schema: RLSSchema, options: MigrationOptions = {}): string { const { name = 'rls_policies', includeContextFunctions = true, ...generatorOptions } = options const upStatements = this.generator.generateStatements(schema, generatorOptions) const downStatements = this.generator.generateDropStatements(schema, generatorOptions) const contextFunctions = includeContextFunctions ? this.generator.generateContextFunctions() : '' return `import { Kysely, sql } from 'kysely'; /** * Migration: ${name} * Generated by @kysera/rls * * This migration sets up Row-Level Security policies for the database. */ export async function up(db: Kysely): Promise { ${ includeContextFunctions ? ` // Create RLS context functions await sql.raw(\`${this.escapeTemplate(contextFunctions)}\`).execute(db); ` : '' } // Enable RLS and create policies ${upStatements.map(s => ` await sql.raw(\`${this.escapeTemplate(s)}\`).execute(db);`).join('\n')} } export async function down(db: Kysely): Promise { ${downStatements.map(s => ` await sql.raw(\`${this.escapeTemplate(s)}\`).execute(db);`).join('\n')} ${ includeContextFunctions ? ` // Drop RLS context functions await sql.raw(\` DROP FUNCTION IF EXISTS rls_current_user_id(); DROP FUNCTION IF EXISTS rls_current_tenant_id(); DROP FUNCTION IF EXISTS rls_current_roles(); DROP FUNCTION IF EXISTS rls_has_role(text); DROP FUNCTION IF EXISTS rls_current_permissions(); DROP FUNCTION IF EXISTS rls_has_permission(text); DROP FUNCTION IF EXISTS rls_is_system(); \`).execute(db);` : '' } } ` } /** * Escape template literal for embedding in string */ private escapeTemplate(str: string): string { return str.replace(/`/g, '\\`').replace(/\$/g, '\\$') } /** * Generate migration filename with timestamp */ generateFilename(name = 'rls_policies'): string { const timestamp = new Date() .toISOString() .replace(/[-:]/g, '') .replace('T', '_') .replace(/\..+/, '') return `${timestamp}_${name}.ts` } }