Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
482
cloud/maplefile-backend/internal/repo/collection/get.go
Normal file
482
cloud/maplefile-backend/internal/repo/collection/get.go
Normal file
|
|
@ -0,0 +1,482 @@
|
|||
// 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue