Skip to content

feat: add group permission storage models#1956

Merged
benjamin-747 merged 1 commit into
gitmono-dev:mainfrom
AllureCurtain:group
Feb 25, 2026
Merged

feat: add group permission storage models#1956
benjamin-747 merged 1 commit into
gitmono-dev:mainfrom
AllureCurtain:group

Conversation

@AllureCurtain

Copy link
Copy Markdown
Contributor

link #1935

@github-actions

github-actions Bot commented Feb 25, 2026

Copy link
Copy Markdown

Claude finished @AllureCurtain's task in 2m 32s —— View job


Code Review: feat: add group permission storage models

  • Gather context and read changed files
  • Review entity models (mega_group, mega_group_member, mega_resource_permission)
  • Review migration file
  • Review storage layer (group_storage.rs)
  • Review module registrations and DTO
  • Post comprehensive review

Overall the PR is clean and follows the established SeaORM patterns in the codebase. The structure is sound. Below are my findings organized by severity.

Summary

Category Findings
Correctness 1 issue (post-transaction consistency)
Design concerns 4 issues
Performance 2 issues
Missing coverage 2 items
Minor / style 3 items

Correctness

1. List query runs outside transaction in replace_resource_permissions and upsert_resource_permissions

jupiter/src/storage/group_storage.rs lines 265–266 and 316–317:

txn.commit().await?;
self.list_resource_permissions(resource_type, resource_id).await  // outside txn

The final list_resource_permissions call is executed after txn.commit() using a separate connection. A concurrent write between commit() and this query could result in returning stale or inconsistent data to the caller. Since the method is named replace_resource_permissions (i.e., semantically atomic), callers may reasonably expect the returned list to reflect exactly what was just written.

Consider either accepting that the return is a best-effort snapshot, documenting it clearly, or collecting the written records in-memory and returning them directly. Fix this →


Design Concerns

2. delete_group_with_relations manually deletes related records that CASCADE FK already covers

jupiter/src/storage/group_storage.rs lines 89–105 / migration lines 60–65, 115–123:

Both mega_group_member and mega_resource_permission foreign keys are set with on_delete(ForeignKeyAction::Cascade). The explicit pre-deletion of members and permissions is therefore redundant for PostgreSQL and MySQL. For SQLite, foreign key enforcement requires PRAGMA foreign_keys = ON to be set per-connection. If the app does not guarantee that, the explicit deletes are a correct safety fallback — but this should be documented.

If relying on cascade, the delete_group_with_relations method could simply delete the group and trust the DB. If supporting SQLite without FK pragmas, document why the manual deletes exist. The DeleteGroupStats struct currently returns three separate counts, which is only useful if the explicit deletes are kept. Fix this →

3. No validation on input strings — name / username / resource_id

jupiter/src/storage/group_storage.rs lines 38–48, 119–158:

create_group accepts any payload.name including empty string. add_group_members accepts any username value (the normalize_usernames helper strips whitespace and deduplicates, but doesn't reject empty-after-trim entries — wait, it does filter empty with .filter(|username| !username.is_empty()), so usernames are fine). However, group name is not validated for emptiness or length before hitting the DB. An empty name would violate the UNIQUE constraint on an empty string, producing an opaque DB error. Recommend adding a guard in create_group:

if payload.name.trim().is_empty() {
    return Err(MegaError::with_message("Group name cannot be empty", 400));
}

4. ResourceTypeEnum has only one variant (Note) but is used as a general permission abstraction

jupiter/callisto/src/sea_orm_active_enums.rs lines 173–177:

This is fine for now, but adding new variants to a PostgreSQL ENUM type requires an ALTER TYPE … ADD VALUE migration. This is non-transactional on PostgreSQL (cannot be done inside a transaction). Teams should be aware of this constraint when extending the enum. Consider adding a comment or noting it in the issue.

5. No update_group method

jupiter/src/storage/group_storage.rs: The storage exposes create, read, delete, and member/permission management, but there is no method to update a group's name or description. If this is intentional (immutable after creation), document it. Otherwise it's a missing operation. Fix this →


Performance

6. find_groups_by_username uses two sequential queries

jupiter/src/storage/group_storage.rs lines 200–213:

let group_ids = self.find_group_ids_by_username(username).await?;  // query 1
// ...
mega_group::Entity::find()
    .filter(mega_group::Column::Id.is_in(group_ids))               // query 2
    .all(self.get_connection()).await?

This could be a single query using a subquery or JOIN. With large numbers of groups, the is_in clause with many IDs may also be inefficient. A JOIN via SeaORM's join API or a raw subquery would be cleaner. Fix this →

7. normalize_permission_bindings has non-deterministic insert order

jupiter/src/storage/group_storage.rs lines 383–398:

The HashMap iteration order is non-deterministic. While the returned list is always sorted by list_resource_permissions (ORDER BY group_id), the insert order into the DB is random. This is harmless functionally but could cause surprising behaviour during debugging or in databases that don't guarantee a stable row ordering. Consider using a BTreeMap to get deterministic key ordering during normalization.


Missing Test Coverage

8. No tests for GroupStorage operations

The PR only adds GroupStorage to test_storage() in tests.rs but includes zero tests for the new storage methods. The existing migration test (test_apply_migrations) covers that the schema is created correctly, but none of the CRUD operations are tested. Given the complexity of replace_resource_permissions and upsert_resource_permissions (with upsert conflict handling), tests are strongly recommended. Fix this →

9. No semantic relations defined between entities

All three entity Relation enums are empty. While the FK constraints exist in the DB, SeaORM relations are not wired up, so find_related / find_with_related API cannot be used. This forces hand-rolled multi-query patterns like find_groups_by_username. This isn't a bug today, but will become a maintenance issue as more cross-entity queries are needed.


Minor / Style

10. normalize_usernames allocates String twice per entry

jupiter/src/storage/group_storage.rs line 374:

.filter_map(|username| {
    if seen.insert(username.to_string()) {  // allocation 1
        Some(username.to_string())           // allocation 2
    } else {
        None
    }
})

Both seen.insert and Some(...) clone the trimmed &str. This can be simplified:

.filter_map(|username| {
    let s = username.to_string();
    seen.insert(s.clone()).then_some(s)
})

11. Migration module doc comment is not updated

jupiter/src/migration/mod.rs lines 12–17: The module-level doc comment lists migration files up to m20250618_065050_add_label but does not include the new m20260216_013852_create_group_permission_tables. Small but worth keeping consistent.

12. pub base: BaseStorage field exposes internal storage directly

jupiter/src/storage/group_storage.rs line 23: The Deref impl and pub base field both expose BaseStorage. The pub base allows callers to bypass the Deref and access the underlying connection directly. This is consistent with other storage structs in the codebase, so not a new concern — just noting it.


What's Done Well

  • Clean SeaORM entity definitions generated by sea-orm-codegen
  • Proper upsert with OnConflict::do_nothing for idempotent member adds
  • normalize_usernames correctly handles whitespace and deduplication
  • replace_resource_permissions uses a transaction for atomicity
  • upsert_resource_permissions uses update_columns on conflict correctly
  • Foreign key cascade wired correctly in migration
  • Unique constraints and indexes are well thought out (idx_group_member_unique, idx_resource_permission_unique)
  • saturating_sub(1) on page number prevents underflow
  • PostgreSQL enum type creation/drop is handled correctly in up/down migrations
  • delete_group_with_relations returns structured stats rather than a bare count

@genedna

genedna commented Feb 25, 2026

Copy link
Copy Markdown
Collaborator

@codex Review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements the foundational database schema and storage layer for a group-based permission system, addressing issue #1935. The changes introduce three new database tables (mega_group, mega_group_member, mega_resource_permission) with their corresponding SeaORM entity models and a comprehensive storage layer for managing groups, group memberships, and resource permissions.

Changes:

  • Added database migration creating three tables for groups, group members, and resource permissions with proper foreign key constraints and indexes
  • Created SeaORM entity models and enum types for PermissionEnum (read/write/admin) and ResourceTypeEnum (note)
  • Implemented GroupStorage with methods for CRUD operations on groups, group membership management, and resource permission binding

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
jupiter/src/migration/m20260216_013852_create_group_permission_tables.rs Migration creating mega_group, mega_group_member, and mega_resource_permission tables with indexes and foreign keys
jupiter/callisto/src/mega_group.rs SeaORM entity model for groups table
jupiter/callisto/src/mega_group_member.rs SeaORM entity model for group membership table
jupiter/callisto/src/mega_resource_permission.rs SeaORM entity model for resource permissions table
jupiter/callisto/src/sea_orm_active_enums.rs Added PermissionEnum and ResourceTypeEnum enums
jupiter/callisto/src/prelude.rs Exported new entity types in prelude
jupiter/callisto/src/mod.rs Added module declarations for new entities
jupiter/src/storage/group_storage.rs Storage layer implementation with group and permission management operations
jupiter/src/model/group_dto.rs DTO definitions for group operations
jupiter/src/model/mod.rs Added group_dto module declaration
jupiter/src/storage/mod.rs Integrated GroupStorage into Storage and AppService structs
jupiter/src/migration/mod.rs Registered new migration in migrator
jupiter/src/tests.rs Added GroupStorage initialization in test helper

Comment thread jupiter/callisto/src/mega_group_member.rs
Comment thread jupiter/callisto/src/mega_resource_permission.rs
Comment thread jupiter/src/storage/group_storage.rs Outdated
.await?)
}
}

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new GroupStorage functionality lacks test coverage. Other storage implementations in the codebase (e.g., build_trigger_storage.rs, dynamic_sidebar_storage.rs) include comprehensive unit tests for their core operations. Consider adding tests for critical operations like create_group, add_group_members (with duplicates), replace_resource_permissions, and the normalization functions.

Suggested change
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_usernames_trims_and_deduplicates() {
let input = vec![
" alice ".to_string(),
"".to_string(),
"bob".to_string(),
"alice".to_string(),
" ".to_string(),
"ALICE".to_string(),
];
let result = normalize_usernames(&input);
// Should trim whitespace and drop empty / whitespace-only entries.
// Deduplication is exact (case-sensitive) and preserves the first occurrence order.
assert_eq!(
result,
vec![
"alice".to_string(),
"bob".to_string(),
"ALICE".to_string(),
]
);
}
#[test]
fn normalize_usernames_handles_empty_input() {
let input: Vec<String> = Vec::new();
let result = normalize_usernames(&input);
assert!(result.is_empty());
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +143 to +152
manager
.create_index(
Index::create()
.if_not_exists()
.name("idx_resource_permission_group_id")
.table(MegaResourcePermission::Table)
.col(MegaResourcePermission::GroupId)
.to_owned(),
)
.await?;

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a composite index on (resource_type, resource_id) for mega_resource_permission table. The list_resource_permissions method at lines 216-227 queries by these two columns, and while a full table scan or the unique index idx_resource_permission_unique can be used, a dedicated composite index would optimize this common query pattern more efficiently.

Copilot uses AI. Check for mistakes.
Comment thread jupiter/src/storage/group_storage.rs Outdated
storage::base_storage::{BaseStorage, StorageConnector},
};

#[derive(Clone)]

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GroupStorage struct is missing the Debug derive that other storage structs have (e.g., UserStorage at user_storage.rs:12). This is inconsistent with the codebase convention and could make debugging more difficult.

Suggested change
#[derive(Clone)]
#[derive(Clone, Debug)]

Copilot uses AI. Check for mistakes.
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Signed-off-by: allure <1550220889@qq.com>
@github-actions

github-actions Bot commented Feb 25, 2026

Copy link
Copy Markdown

Claude finished @AllureCurtain's task in 2m 9s —— View job


Code Review: feat: add group permission storage models

  • Gather context and read CLAUDE.md
  • Review changed files in PR
  • Analyze code quality, security, performance, and testing
  • Post comprehensive review feedback

Overall the PR is well-structured and follows established SeaORM patterns in the codebase. I noticed some items have already been addressed compared to the previous review (e.g., replace_resource_permissions now correctly reads within the transaction, and normalize_permission_bindings already uses BTreeMap). Below are the remaining issues.


Correctness

1. upsert_resource_permissions reads outside the transaction

jupiter/src/storage/group_storage.rs lines 313–323:

match mega_resource_permission::Entity::insert_many(models)
    .on_conflict(on_conflict)
    .exec(self.get_connection())  // no transaction
    .await { ... }

self.list_resource_permissions(resource_type, resource_id).await  // second connection, no transaction

Unlike replace_resource_permissions (which correctly reads within the transaction), upsert_resource_permissions does neither the write nor the final list read inside a transaction. A concurrent write between these two calls could return stale data. Consider wrapping in a transaction or documenting the eventual-consistency behavior. Fix this →


Design Concerns

2. No validation on group name in create_group

jupiter/src/storage/group_storage.rs lines 35–49:

An empty name string passes through to the DB, where it would hit the UNIQUE constraint on mega_group.name and produce an opaque DB error. Adding a guard would give callers a clear error message:

if payload.name.trim().is_empty() {
    return Err(MegaError::with_message("Group name cannot be empty", 400));
}

3. Empty SeaORM Relation enums on all three entities

jupiter/callisto/src/mega_group_member.rs:16, mega_resource_permission.rs:21:

The FK constraints exist at the DB level but SeaORM relation traversal (find_related, find_with_related) is not wired up. This forces multi-query patterns like find_groups_by_username (two queries instead of one JOIN). This isn't a bug today, but will compound as more cross-entity queries are needed. Fix this →

4. delete_group_with_relations silently redundant pre-deletes without documentation

jupiter/src/storage/group_storage.rs lines 89–105:

The explicit pre-deletion of members and permissions is redundant for PostgreSQL/MySQL given the ON DELETE CASCADE FK. For SQLite, FK enforcement requires PRAGMA foreign_keys = ON per-connection. If SQLite support is guaranteed with the pragma, these deletes can be removed. If not, add a comment explaining the safety fallback so future readers don't remove it thinking it's dead code. Fix this →

5. No update_group method

The storage layer has create, read, and delete for groups, but no method to update name or description. If this is intentional (groups are immutable after creation), document it. Otherwise it's a missing CRUD operation that callers will need to work around.


Performance

6. find_groups_by_username uses two sequential queries

jupiter/src/storage/group_storage.rs lines 200–214:

let group_ids = self.find_group_ids_by_username(username).await?;  // query 1
mega_group::Entity::find()
    .filter(mega_group::Column::Id.is_in(group_ids))                // query 2

This could be a single query using a JOIN or subquery. With large group counts, the is_in clause with many IDs may also degrade. Fix this →

7. Unnecessary .to_vec() allocation in find_permissions_by_resource

jupiter/src/storage/group_storage.rs line 365:

.filter(mega_resource_permission::Column::GroupId.is_in(group_ids.to_vec()))

group_ids is already &[i64] which is_in can accept directly. The .to_vec() creates an unnecessary heap allocation.

8. Missing (resource_type, resource_id) composite index

jupiter/src/migration/m20260216_013852_create_group_permission_tables.rs:

list_resource_permissions queries on (resource_type, resource_id) but the only covering index is idx_resource_permission_unique which is a 3-column index on (resource_type, resource_id, group_id). While the 3-column index can serve the query, a dedicated 2-column index would be more efficient for range scans on those two columns. The existing idx_resource_permission_group_id index covers group-id lookups but not this pattern.


Missing Test Coverage

9. No tests for GroupStorage operations

The PR only registers GroupStorage in the test helper but includes zero tests for the new storage methods. The upsert conflict paths in replace_resource_permissions and upsert_resource_permissions are particularly important to test given their complexity. Fix this →


Minor / Style

10. Migration module doc comment not updated

jupiter/src/migration/mod.rs lines 12–18: The doc comment listing migration files ends at m20250618_065050_add_label but the new m20260216_013852_create_group_permission_tables migration is not listed.

11. ResourceTypeEnum extensibility caveat

jupiter/callisto/src/sea_orm_active_enums.rs lines 172–177: Currently only has Note. Adding new variants to a PostgreSQL ENUM type via ALTER TYPE … ADD VALUE is non-transactional. Teams should be aware of this when planning to extend the permission model.


What's Done Well

  • replace_resource_permissions correctly reads within the transaction (the list query uses &txn before commit())
  • normalize_permission_bindings uses BTreeMap giving deterministic key ordering
  • Proper upsert with OnConflict::do_nothing for idempotent member adds
  • normalize_usernames correctly handles whitespace, empty strings, and deduplication without double-allocation
  • #[derive(Clone, Debug)] is present on GroupStorage — consistent with the codebase
  • Foreign key cascades wired correctly in migration
  • Unique constraints and indexes (idx_group_member_unique, idx_resource_permission_unique) are well thought out
  • saturating_sub(1) on page number prevents underflow
  • PostgreSQL enum type creation/drop is handled correctly in up/down migrations

@benjamin-747 benjamin-747 added this pull request to the merge queue Feb 25, 2026
Merged via the queue into gitmono-dev:main with commit cce1fbf Feb 25, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants