// monorepo/cloud/maplefile-backend/internal/maplefile/repo/collection/get.go package collection import ( "context" "fmt" "time" "github.com/gocql/gocql" dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection" "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation" "go.uber.org/zap" ) // Core helper methods for loading collections with members func (impl *collectionRepositoryImpl) loadCollectionWithMembers(ctx context.Context, collectionID gocql.UUID) (*dom_collection.Collection, error) { // 1. Load base collection collection, err := impl.getBaseCollection(ctx, collectionID) if err != nil || collection == nil { return collection, err } // 2. Load and populate members members, err := impl.getCollectionMembers(ctx, collectionID) if err != nil { return nil, err } collection.Members = members return collection, nil } func (impl *collectionRepositoryImpl) getBaseCollection(ctx context.Context, id gocql.UUID) (*dom_collection.Collection, error) { var ( encryptedName, collectionType, encryptedKeyJSON string encryptedCustomIcon string ancestorIDsJSON string tagsJSON string parentID, ownerID, createdByUserID, modifiedByUserID gocql.UUID createdAt, modifiedAt, tombstoneExpiry time.Time version, tombstoneVersion uint64 state string fileCount int64 ) query := `SELECT id, owner_id, encrypted_name, collection_type, encrypted_collection_key, encrypted_custom_icon, parent_id, ancestor_ids, file_count, tags, created_at, created_by_user_id, modified_at, modified_by_user_id, version, state, tombstone_version, tombstone_expiry FROM collections_by_id WHERE id = ?` err := impl.Session.Query(query, id).WithContext(ctx).Scan( &id, &ownerID, &encryptedName, &collectionType, &encryptedKeyJSON, &encryptedCustomIcon, &parentID, &ancestorIDsJSON, &fileCount, &tagsJSON, &createdAt, &createdByUserID, &modifiedAt, &modifiedByUserID, &version, &state, &tombstoneVersion, &tombstoneExpiry) if err != nil { if err == gocql.ErrNotFound { return nil, nil } return nil, fmt.Errorf("failed to get collection: %w", err) } // Deserialize complex fields ancestorIDs, err := impl.deserializeAncestorIDs(ancestorIDsJSON) if err != nil { return nil, fmt.Errorf("failed to deserialize ancestor IDs: %w", err) } encryptedKey, err := impl.deserializeEncryptedCollectionKey(encryptedKeyJSON) if err != nil { return nil, fmt.Errorf("failed to deserialize encrypted collection key: %w", err) } tags, err := impl.deserializeTags(tagsJSON) if err != nil { return nil, fmt.Errorf("failed to deserialize tags: %w", err) } collection := &dom_collection.Collection{ ID: id, OwnerID: ownerID, EncryptedName: encryptedName, CollectionType: collectionType, EncryptedCollectionKey: encryptedKey, EncryptedCustomIcon: encryptedCustomIcon, Members: []dom_collection.CollectionMembership{}, // Will be populated separately ParentID: parentID, AncestorIDs: ancestorIDs, FileCount: fileCount, Tags: tags, CreatedAt: createdAt, CreatedByUserID: createdByUserID, ModifiedAt: modifiedAt, ModifiedByUserID: modifiedByUserID, Version: version, State: state, TombstoneVersion: tombstoneVersion, TombstoneExpiry: tombstoneExpiry, } return collection, nil } func (impl *collectionRepositoryImpl) getCollectionMembers(ctx context.Context, collectionID gocql.UUID) ([]dom_collection.CollectionMembership, error) { var members []dom_collection.CollectionMembership query := `SELECT recipient_id, member_id, recipient_email, granted_by_id, encrypted_collection_key, permission_level, created_at, is_inherited, inherited_from_id FROM collection_members_by_collection_id_and_recipient_id WHERE collection_id = ?` impl.Logger.Info("🔍 GET MEMBERS: Querying collection members", zap.String("collection_id", collectionID.String()), zap.String("query", query)) iter := impl.Session.Query(query, collectionID).WithContext(ctx).Iter() var ( recipientID, memberID, grantedByID, inheritedFromID gocql.UUID recipientEmail, permissionLevel string encryptedCollectionKey []byte createdAt time.Time isInherited bool ) for iter.Scan(&recipientID, &memberID, &recipientEmail, &grantedByID, &encryptedCollectionKey, &permissionLevel, &createdAt, &isInherited, &inheritedFromID) { impl.Logger.Info("🔍 GET MEMBERS: Found member", zap.String("collection_id", collectionID.String()), zap.String("recipient_email", validation.MaskEmail(recipientEmail)), zap.String("recipient_id", recipientID.String()), zap.Int("encrypted_key_length", len(encryptedCollectionKey)), zap.String("permission_level", permissionLevel)) member := dom_collection.CollectionMembership{ ID: memberID, CollectionID: collectionID, RecipientID: recipientID, RecipientEmail: recipientEmail, GrantedByID: grantedByID, EncryptedCollectionKey: encryptedCollectionKey, PermissionLevel: permissionLevel, CreatedAt: createdAt, IsInherited: isInherited, InheritedFromID: inheritedFromID, } members = append(members, member) } if err := iter.Close(); err != nil { impl.Logger.Error("🔍 GET MEMBERS: Failed to iterate members", zap.String("collection_id", collectionID.String()), zap.Error(err)) return nil, err } impl.Logger.Info("🔍 GET MEMBERS: Query completed", zap.String("collection_id", collectionID.String()), zap.Int("members_found", len(members))) return members, nil } func (impl *collectionRepositoryImpl) loadMultipleCollectionsWithMembers(ctx context.Context, collectionIDs []gocql.UUID) ([]*dom_collection.Collection, error) { if len(collectionIDs) == 0 { return []*dom_collection.Collection{}, nil } var collections []*dom_collection.Collection for _, id := range collectionIDs { collection, err := impl.loadCollectionWithMembers(ctx, id) if err != nil { impl.Logger.Warn("failed to load collection", zap.String("collection_id", id.String()), zap.Error(err)) continue } if collection != nil { collections = append(collections, collection) } } return collections, nil } func (impl *collectionRepositoryImpl) Get(ctx context.Context, id gocql.UUID) (*dom_collection.Collection, error) { return impl.loadCollectionWithMembers(ctx, id) } // FIXED: Removed state filtering from query, filter in memory instead func (impl *collectionRepositoryImpl) GetAllByUserID(ctx context.Context, ownerID gocql.UUID) ([]*dom_collection.Collection, error) { var collectionIDs []gocql.UUID query := `SELECT collection_id FROM collections_by_user_id_and_access_type_with_desc_modified_at_and_asc_collection_id WHERE user_id = ? AND access_type = 'owner'` iter := impl.Session.Query(query, ownerID).WithContext(ctx).Iter() var collectionID gocql.UUID for iter.Scan(&collectionID) { collectionIDs = append(collectionIDs, collectionID) } if err := iter.Close(); err != nil { impl.Logger.Error("failed to get collections", zap.Any("user_id", ownerID), zap.Error(err), ) return nil, fmt.Errorf("failed to get collections by owner: %w", err) } // Load collections and filter by state in memory allCollections, err := impl.loadMultipleCollectionsWithMembers(ctx, collectionIDs) if err != nil { return nil, err } // Filter to only active collections var activeCollections []*dom_collection.Collection for _, collection := range allCollections { if collection.State == dom_collection.CollectionStateActive { activeCollections = append(activeCollections, collection) } } impl.Logger.Debug("retrieved owned collections efficiently", zap.String("owner_id", ownerID.String()), zap.Int("total_found", len(allCollections)), zap.Int("active_count", len(activeCollections))) return activeCollections, nil } // FIXED: Removed state filtering from query, filter in memory instead func (impl *collectionRepositoryImpl) GetCollectionsSharedWithUser(ctx context.Context, userID gocql.UUID) ([]*dom_collection.Collection, error) { impl.Logger.Info("🔍 REPO: Getting collections shared with user", zap.String("user_id", userID.String())) var collectionIDs []gocql.UUID query := `SELECT collection_id FROM collections_by_user_id_and_access_type_with_desc_modified_at_and_asc_collection_id WHERE user_id = ? AND access_type = 'member'` impl.Logger.Info("🔍 REPO: Executing query", zap.String("user_id", userID.String()), zap.String("query", query)) iter := impl.Session.Query(query, userID).WithContext(ctx).Iter() var collectionID gocql.UUID for iter.Scan(&collectionID) { collectionIDs = append(collectionIDs, collectionID) impl.Logger.Info("🔍 REPO: Found collection ID in index", zap.String("collection_id", collectionID.String())) } if err := iter.Close(); err != nil { impl.Logger.Error("🔍 REPO: Query iteration failed", zap.String("user_id", userID.String()), zap.Error(err)) return nil, fmt.Errorf("failed to get shared collections: %w", err) } impl.Logger.Info("🔍 REPO: Query completed", zap.String("user_id", userID.String()), zap.Int("collection_ids_found", len(collectionIDs))) // Load collections and filter by state in memory allCollections, err := impl.loadMultipleCollectionsWithMembers(ctx, collectionIDs) if err != nil { impl.Logger.Error("🔍 REPO: Failed to load collections", zap.String("user_id", userID.String()), zap.Error(err)) return nil, err } impl.Logger.Info("🔍 REPO: Loaded collections", zap.String("user_id", userID.String()), zap.Int("collections_loaded", len(allCollections))) // Filter to only active collections AND collections where the user has actual membership var activeCollections []*dom_collection.Collection for _, collection := range allCollections { impl.Logger.Info("🔍 REPO: Checking collection state", zap.String("collection_id", collection.ID.String()), zap.String("state", collection.State), zap.Bool("is_active", collection.State == dom_collection.CollectionStateActive)) if collection.State != dom_collection.CollectionStateActive { impl.Logger.Info("🔍 REPO: Skipping inactive collection", zap.String("collection_id", collection.ID.String()), zap.String("state", collection.State)) continue } // Check if the user has actual membership in this collection // For GetCollectionsSharedWithUser, we MUST have a membership record // This is the source of truth, not the index hasMembership := false for _, member := range collection.Members { if member.RecipientID == userID { hasMembership = true impl.Logger.Info("🔍 REPO: User has membership in collection", zap.String("collection_id", collection.ID.String()), zap.String("user_id", userID.String()), zap.String("recipient_email", validation.MaskEmail(member.RecipientEmail)), zap.String("permission_level", member.PermissionLevel)) break } } if !hasMembership { // No actual membership record found - this is stale index data // Skip this collection regardless of ownership impl.Logger.Warn("🔍 REPO: Skipping collection with no actual membership (stale index)", zap.String("collection_id", collection.ID.String()), zap.String("user_id", userID.String()), zap.Bool("is_owner", collection.OwnerID == userID), zap.Int("members_count", len(collection.Members))) continue } activeCollections = append(activeCollections, collection) } impl.Logger.Debug("retrieved shared collections efficiently", zap.String("user_id", userID.String()), zap.Int("total_found", len(allCollections)), zap.Int("active_count", len(activeCollections))) return activeCollections, nil } // NEW METHOD: Demonstrates querying across all access types when needed func (impl *collectionRepositoryImpl) GetAllUserCollections(ctx context.Context, userID gocql.UUID) ([]*dom_collection.Collection, error) { var collectionIDs []gocql.UUID query := `SELECT collection_id FROM collections_by_user_id_with_desc_modified_at_and_asc_collection_id WHERE user_id = ?` iter := impl.Session.Query(query, userID).WithContext(ctx).Iter() var collectionID gocql.UUID for iter.Scan(&collectionID) { collectionIDs = append(collectionIDs, collectionID) } if err := iter.Close(); err != nil { return nil, fmt.Errorf("failed to get all user collections: %w", err) } // Load collections and filter by state in memory allCollections, err := impl.loadMultipleCollectionsWithMembers(ctx, collectionIDs) if err != nil { return nil, err } // Filter to only active collections var activeCollections []*dom_collection.Collection for _, collection := range allCollections { if collection.State == dom_collection.CollectionStateActive { activeCollections = append(activeCollections, collection) } } impl.Logger.Debug("retrieved all user collections efficiently", zap.String("user_id", userID.String()), zap.Int("total_found", len(allCollections)), zap.Int("active_count", len(activeCollections))) return activeCollections, nil } // Uses composite partition key table for better performance func (impl *collectionRepositoryImpl) FindByParent(ctx context.Context, parentID gocql.UUID) ([]*dom_collection.Collection, error) { var collectionIDs []gocql.UUID query := `SELECT collection_id FROM collections_by_parent_id_with_asc_created_at_and_asc_collection_id WHERE parent_id = ?` iter := impl.Session.Query(query, parentID).WithContext(ctx).Iter() var collectionID gocql.UUID for iter.Scan(&collectionID) { collectionIDs = append(collectionIDs, collectionID) } if err := iter.Close(); err != nil { return nil, fmt.Errorf("failed to find collections by parent: %w", err) } // Load collections and filter by state in memory allCollections, err := impl.loadMultipleCollectionsWithMembers(ctx, collectionIDs) if err != nil { return nil, err } // Filter to only active collections var activeCollections []*dom_collection.Collection for _, collection := range allCollections { if collection.State == dom_collection.CollectionStateActive { activeCollections = append(activeCollections, collection) } } return activeCollections, nil } // Uses composite partition key for optimal performance func (impl *collectionRepositoryImpl) FindRootCollections(ctx context.Context, ownerID gocql.UUID) ([]*dom_collection.Collection, error) { var collectionIDs []gocql.UUID // Use the composite partition key table for root collections nullParentID := impl.nullParentUUID() query := `SELECT collection_id FROM collections_by_parent_and_owner_id_with_asc_created_at_and_asc_collection_id WHERE parent_id = ? AND owner_id = ?` iter := impl.Session.Query(query, nullParentID, ownerID).WithContext(ctx).Iter() var collectionID gocql.UUID for iter.Scan(&collectionID) { collectionIDs = append(collectionIDs, collectionID) } if err := iter.Close(); err != nil { return nil, fmt.Errorf("failed to find root collections: %w", err) } // Load collections and filter by state in memory allCollections, err := impl.loadMultipleCollectionsWithMembers(ctx, collectionIDs) if err != nil { return nil, err } // Filter to only active collections var activeCollections []*dom_collection.Collection for _, collection := range allCollections { if collection.State == dom_collection.CollectionStateActive { activeCollections = append(activeCollections, collection) } } return activeCollections, nil } // No more recursive queries - single efficient query func (impl *collectionRepositoryImpl) FindDescendants(ctx context.Context, collectionID gocql.UUID) ([]*dom_collection.Collection, error) { var descendantIDs []gocql.UUID query := `SELECT collection_id FROM collections_by_ancestor_id_with_asc_depth_and_asc_collection_id WHERE ancestor_id = ?` iter := impl.Session.Query(query, collectionID).WithContext(ctx).Iter() var descendantID gocql.UUID for iter.Scan(&descendantID) { descendantIDs = append(descendantIDs, descendantID) } if err := iter.Close(); err != nil { return nil, fmt.Errorf("failed to find descendants: %w", err) } // Load collections and filter by state in memory allCollections, err := impl.loadMultipleCollectionsWithMembers(ctx, descendantIDs) if err != nil { return nil, err } // Filter to only active collections var activeCollections []*dom_collection.Collection for _, collection := range allCollections { if collection.State == dom_collection.CollectionStateActive { activeCollections = append(activeCollections, collection) } } return activeCollections, nil }