UNPKG

@shoito/prismarls

Version:

Prismarls is a CLI tool designed to facilitate the integration of PostgreSQL Row-Level Security (RLS) with Prisma Migrations. After creating a migration using `prisma migrate dev --create-only`, running `prismarls` will automatically generate SQL to appen

80 lines (79 loc) 4.37 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.appendRlsSettingsToMigration = exports.extractRlsFields = void 0; const node_fs_1 = __importDefault(require("node:fs")); const prisma_schema_parser_1 = require("@loancrate/prisma-schema-parser"); // Extracts RLS fields from the schema file function extractRlsFields(schemaFile) { const declarations = (0, prisma_schema_parser_1.parsePrismaSchema)(node_fs_1.default.readFileSync(schemaFile, { encoding: "utf8" })).declarations.filter((declaration) => declaration.kind === "model"); return declarations .flatMap((model) => model.members .filter((member) => { var _a; return member.kind === "field" && ((_a = member.comment) === null || _a === void 0 ? void 0 : _a.text.includes("@RLS")); }) .map((f) => { var _a, _b; const field = f; const comment = (_b = (_a = field.comment) === null || _a === void 0 ? void 0 : _a.text) !== null && _b !== void 0 ? _b : ""; const tableMatch = comment.match(/table: "([^"]+)"/); const columnMatch = comment.match(/column: "([^"]+)"/); return { table: tableMatch ? tableMatch[1] : model.name.value, column: columnMatch ? columnMatch[1] : field.name.value, }; })) .filter((model) => model); } exports.extractRlsFields = extractRlsFields; // Appends RLS settings to the latest migration file function appendRlsSettingsToMigration(rlsModels, migrationsDir, currentSettingIsolation, currentUser, currentSettingBypass, forceEnable) { const latestMigrationDir = findLatestMigrationDirectory(migrationsDir); if (!latestMigrationDir) { console.log("No migration directory found"); return; } const migrationFilePath = `${migrationsDir}/${latestMigrationDir}/migration.sql`; const migrationContent = node_fs_1.default.readFileSync(migrationFilePath, { encoding: "utf8", }); if (migrationContent.includes("ENABLE ROW LEVEL SECURITY")) { console.log("RLS already enabled"); return; } const appendSqls = generateRlsStatements(rlsModels, migrationContent, currentSettingIsolation, currentUser, currentSettingBypass, forceEnable); if (!appendSqls.length) { console.log("No matched tables found"); return; } node_fs_1.default.appendFileSync(migrationFilePath, `\n-- RLS Settings\n${appendSqls.join("\n")}\n`); } exports.appendRlsSettingsToMigration = appendRlsSettingsToMigration; // Finds the latest migration directory based on naming convention function findLatestMigrationDirectory(migrationsDir) { const directories = node_fs_1.default .readdirSync(migrationsDir, { withFileTypes: true }) .filter((dirent) => dirent.isDirectory()) .map((dirent) => dirent.name) .sort((a, b) => b.localeCompare(a)); return directories.length > 0 ? directories[0] : undefined; } // Generates SQL statements for enabling RLS based on existing migrations and RLS models function generateRlsStatements(rlsModels, migrationContent, currentSettingIsolation, currentUser, currentSettingBypass, forceEnable) { const createTablePattern = /CREATE TABLE "([^"]+)"/g; const matchedTables = Array.from(migrationContent.matchAll(createTablePattern), (match) => match[1]); return rlsModels .filter((model) => matchedTables.includes(model.table)) .map((model) => { const alterTableEnable = `ALTER TABLE "${model.table}" ENABLE ROW LEVEL SECURITY;`; const alterTableForce = forceEnable ? `ALTER TABLE "${model.table}" FORCE ROW LEVEL SECURITY;\n` : ""; const createIsolationPolicy = `CREATE POLICY tenant_isolation_policy ON "${model.table}"`; const using = currentUser ? `USING("${model.column}" = current_user);` : `USING("${model.column}" = current_setting('${currentSettingIsolation}'));`; const createBypassPolicy = `CREATE POLICY bypass_rls_policy ON "${model.table}" USING (current_setting('${currentSettingBypass}', TRUE)::text = 'on');`; return `${alterTableEnable}\n${alterTableForce}${createIsolationPolicy} ${using}\n${createBypassPolicy}`; }); }