@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
JavaScript
;
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}`;
});
}