1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2026-02-05 04:34:03 +00:00

Correct comment functionality, add comment constraint exception

This commit is contained in:
lukinovec 2025-05-21 11:25:09 +02:00
parent a52745704e
commit f65c64c9c7
3 changed files with 83 additions and 110 deletions

View file

@ -7,6 +7,7 @@ namespace Stancl\Tenancy\RLS\PolicyManagers;
use Illuminate\Database\DatabaseManager;
use Illuminate\Support\Str;
use Stancl\Tenancy\Database\Exceptions\RecursiveRelationshipException;
use Stancl\Tenancy\Exceptions\RLSCommentConstraintException;
// todo@samuel logical + structural refactor. the tree generation could use some dynamic programming optimizations
class TableRLSManager implements RLSPolicyManager
@ -81,7 +82,7 @@ class TableRLSManager implements RLSPolicyManager
$table = str($table)->afterLast('.')->toString();
// For each table, we get a list of all foreign key columns
$foreignKeys = collect($builder->getForeignKeys($table))->merge($this->getFakeForeignKeys($table))->map(function ($foreign) use ($table) {
$foreignKeys = collect($builder->getForeignKeys($table))->merge($this->getCommentConstraints($table))->map(function ($foreign) use ($table) {
return $this->formatForeignKey($foreign, $table);
});
@ -111,7 +112,7 @@ class TableRLSManager implements RLSPolicyManager
{
// If the foreign key has a comment of 'no-rls', we skip it
// Also skip the foreign key if implicit scoping is off and the foreign key has no comment
if ($foreign['comment'] === 'no-rls' || (! static::$scopeByDefault && $foreign['comment'] === null)) {
if ($this->shouldSkipPathLeadingThrough($foreign)) {
return;
}
@ -125,48 +126,79 @@ class TableRLSManager implements RLSPolicyManager
$paths[] = $currentPath;
} else {
// If not, recursively generate paths for the foreign table
foreach (array_merge($this->database->getSchemaBuilder()->getForeignKeys($foreign['foreignTable'], $this->getFakeForeignKeys($table))) as $nextConstraint) {
foreach (array_merge(
$this->database->getSchemaBuilder()->getForeignKeys($foreign['foreignTable']),
$this->getCommentConstraints($foreign['foreignTable'])
) as $nextConstraint) {
$this->generatePaths($table, $this->formatForeignKey($nextConstraint, $foreign['foreignTable']), $paths, $currentPath);
}
}
}
protected function getFakeForeignKeys(string $tableName): array
protected function shouldSkipPathLeadingThrough(array $foreignKey): bool
{
// If the foreign key has a comment of 'no-rls', we skip it
// Also skip the foreign key if implicit scoping is off and the foreign key has no comment
$pathExplicitlySkipped = $foreignKey['comment'] === 'no-rls';
$pathImplicitlySkipped = ! static::$scopeByDefault && (
! isset($foreignKey['comment']) ||
(is_string($foreignKey['comment']) && ! Str::startsWith($foreignKey['comment'], 'rls'))
);
return $pathExplicitlySkipped || $pathImplicitlySkipped;
}
/**
* Retrieve comment-based constraints for a table. These are columns with comments in the format:
* "rls <foreign_table>.<foreign_column>"
*
* Throws an exception if the comment is formatted incorrectly or if the referenced table/column does not exist.
*/
protected function getCommentConstraints(string $tableName): array
{
$columns = $this->database->getSchemaBuilder()->getColumns($tableName);
$schemaBuilder = $this->database->getSchemaBuilder();
$fakeForeignKeys = array_filter($columns, function ($column) {
// Constraint comment should be "rls <foreign_table>.<foreign_column>"
if ($column['comment']) {
return Str::contains($column['comment'], 'rls ');
}
return false;
$commentConstraints = array_filter($columns, function ($column) {
return (isset($column['comment']) && is_string($column['comment']))
&& Str::startsWith($column['comment'], 'rls ');
});
if ($tableName === 'non_constrained_posts') {
dump(array_map(function ($fakeForeignKey) {
// Constraint comment is "rls <foreign_table>.<foreign_column>"
$constraint = explode('.', Str::after($fakeForeignKey['comment'], 'rls '));
return array_map(function ($commentConstraint) use ($schemaBuilder, $tableName) {
$comment = $commentConstraint['comment'];
$constraintString = Str::after($comment, 'rls ');
$constraint = explode('.', $constraintString);
return [
'foreign_table' => $constraint[0],
'foreign_columns' => [$constraint[1]],
'columns' => [$fakeForeignKey['name']],
];
}, $fakeForeignKeys));
}
// Validate comment constraint format
if (count($constraint) !== 2 || empty($constraint[0]) || empty($constraint[1])) {
throw new RLSCommentConstraintException("Incorrectly formatted comment constraint on {$tableName}.{$commentConstraint['name']}: '{$comment}'");
}
return array_map(function ($fakeForeignKey) {
// Constraint comment is "rls <foreign_table>.<foreign_column>"
$constraint = explode('.', Str::after($fakeForeignKey['comment'], 'rls '));
$foreignTable = $constraint[0];
$foreignColumn = $constraint[1];
// Validate table existence
$allTables = array_map(function ($table) {
return str($table)->afterLast('.')->toString();
}, $schemaBuilder->getTableListing(schema: $this->database->getConfig('search_path')));
if (! in_array($foreignTable, $allTables, true)) {
throw new RLSCommentConstraintException("Comment constraint on {$tableName}.{$commentConstraint['name']} references non-existent table '{$foreignTable}'");
}
// Validate column existence
$foreignColumns = $schemaBuilder->getColumns($foreignTable);
$foreignColumnNames = array_column($foreignColumns, 'name');
if (! in_array($foreignColumn, $foreignColumnNames, true)) {
throw new RLSCommentConstraintException("Comment constraint on {$tableName}.{$commentConstraint['name']} references non-existent column '{$foreignTable}.{$foreignColumn}'");
}
return [
'foreign_table' => $constraint[0],
'foreign_columns' => [$constraint[1]],
'columns' => [$fakeForeignKey['name']],
'foreign_table' => $foreignTable,
'foreign_columns' => [$foreignColumn],
'columns' => [$commentConstraint['name']],
];
}, $fakeForeignKeys);
}, $commentConstraints);
}
/** Get tree's non-nullable paths. */
@ -215,7 +247,7 @@ class TableRLSManager implements RLSPolicyManager
}
/**
* Formats the foreign key array retrieved by Postgres to a more readable format.
* Formats the foreign key retrieved by Postgres or comment-based constraint to a more readable format.
*
* Also provides information about whether the foreign key is nullable,
* and the foreign key column comment. These additional details are removed