Files
silo-server/internal/userdb/collections.go
T

309 lines
9.2 KiB
Go

package userdb
import (
"database/sql"
"fmt"
"github.com/Silo-Server/silo-server/internal/userstore"
)
// Collection is an alias for the canonical type in userstore.
type Collection = userstore.Collection
// CollectionItem is an alias for the canonical type in userstore.
type CollectionItem = userstore.CollectionItem
// CreateCollection creates a new personal collection with a generated UUID.
func CreateCollection(db *sql.DB, input userstore.CreateCollectionInput) (*Collection, error) {
id := generateUUID()
now := nowUTC()
if input.CollectionType == "" {
input.CollectionType = "manual"
}
if input.QueryDefinition == "" {
input.QueryDefinition = "{}"
}
if input.SortConfig == "" {
input.SortConfig = "{}"
}
allowedProfiles := normalizeAllowedProfiles(input.CreatorProfileID, input.AllowedProfileIDs, input.IsShared)
tx, err := db.Begin()
if err != nil {
return nil, err
}
defer tx.Rollback()
_, err = tx.Exec(
`INSERT INTO personal_collections (
id, profile_id, creator_profile_id, name, collection_type, is_shared,
query_definition, sort_config, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
id, input.CreatorProfileID, input.CreatorProfileID, input.Name, input.CollectionType, input.IsShared,
input.QueryDefinition, input.SortConfig, now, now,
)
if err != nil {
return nil, err
}
for _, profileID := range allowedProfiles {
if _, err := tx.Exec(
`INSERT INTO personal_collection_profiles (collection_id, profile_id) VALUES (?, ?)`,
id, profileID,
); err != nil {
return nil, err
}
}
if err := tx.Commit(); err != nil {
return nil, err
}
return &Collection{
ID: id,
ProfileID: input.CreatorProfileID,
CreatorProfileID: input.CreatorProfileID,
Name: input.Name,
CollectionType: input.CollectionType,
IsShared: input.IsShared,
AllowedProfileIDs: allowedProfiles,
QueryDefinition: input.QueryDefinition,
SortConfig: input.SortConfig,
CreatedAt: now,
UpdatedAt: now,
}, nil
}
// GetCollection retrieves a collection by its ID.
func GetCollection(db *sql.DB, id string) (*Collection, error) {
var c Collection
var isShared bool
err := db.QueryRow(
`SELECT id, profile_id, creator_profile_id, name, collection_type, is_shared, query_definition, sort_config, created_at, updated_at
FROM personal_collections WHERE id = ?`,
id,
).Scan(&c.ID, &c.ProfileID, &c.CreatorProfileID, &c.Name, &c.CollectionType, &isShared, &c.QueryDefinition, &c.SortConfig, &c.CreatedAt, &c.UpdatedAt)
if err != nil {
return nil, err
}
c.IsShared = isShared
c.AllowedProfileIDs, err = listCollectionProfiles(db, id)
if err != nil {
return nil, err
}
return &c, nil
}
// ListCollections returns all collections for a given profile, ordered by creation date.
func ListCollections(db *sql.DB, profileID string) ([]Collection, error) {
rows, err := db.Query(
`SELECT pc.id, pc.profile_id, pc.creator_profile_id, pc.name, pc.collection_type, pc.is_shared,
pc.query_definition, pc.sort_config, pc.created_at, pc.updated_at
FROM personal_collections pc
JOIN personal_collection_profiles pcp ON pcp.collection_id = pc.id
WHERE pcp.profile_id = ? ORDER BY pc.created_at ASC`,
profileID,
)
if err != nil {
return nil, err
}
defer rows.Close()
var collections []Collection
for rows.Next() {
var c Collection
if err := rows.Scan(&c.ID, &c.ProfileID, &c.CreatorProfileID, &c.Name, &c.CollectionType, &c.IsShared, &c.QueryDefinition, &c.SortConfig, &c.CreatedAt, &c.UpdatedAt); err != nil {
return nil, err
}
c.AllowedProfileIDs, err = listCollectionProfiles(db, c.ID)
if err != nil {
return nil, err
}
collections = append(collections, c)
}
return collections, rows.Err()
}
// UpdateCollection renames a collection and updates its updated_at timestamp.
func UpdateCollection(db *sql.DB, input userstore.UpdateCollectionInput) error {
var creatorProfileID string
if err := db.QueryRow(`SELECT creator_profile_id FROM personal_collections WHERE id = ?`, input.ID).Scan(&creatorProfileID); err != nil {
return err
}
if creatorProfileID != input.RequestProfileID {
return fmt.Errorf("only the creator can update this collection")
}
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
now := nowUTC()
if input.Name != nil {
if _, err := tx.Exec(`UPDATE personal_collections SET name = ?, updated_at = ? WHERE id = ?`, *input.Name, now, input.ID); err != nil {
return err
}
}
if input.IsShared != nil {
if _, err := tx.Exec(`UPDATE personal_collections SET is_shared = ?, updated_at = ? WHERE id = ?`, *input.IsShared, now, input.ID); err != nil {
return err
}
}
if input.QueryDefinition != nil {
if _, err := tx.Exec(`UPDATE personal_collections SET query_definition = ?, updated_at = ? WHERE id = ?`, *input.QueryDefinition, now, input.ID); err != nil {
return err
}
}
if input.SortConfig != nil {
if _, err := tx.Exec(`UPDATE personal_collections SET sort_config = ?, updated_at = ? WHERE id = ?`, *input.SortConfig, now, input.ID); err != nil {
return err
}
}
if input.AllowedProfileIDs != nil || input.IsShared != nil {
isShared := false
if input.IsShared != nil {
isShared = *input.IsShared
} else {
if err := tx.QueryRow(`SELECT is_shared FROM personal_collections WHERE id = ?`, input.ID).Scan(&isShared); err != nil {
return err
}
}
allowed := []string{}
if input.AllowedProfileIDs != nil {
allowed = *input.AllowedProfileIDs
} else {
allowed, err = listCollectionProfilesTx(tx, input.ID)
if err != nil {
return err
}
}
allowed = normalizeAllowedProfiles(creatorProfileID, allowed, isShared)
if _, err := tx.Exec(`DELETE FROM personal_collection_profiles WHERE collection_id = ?`, input.ID); err != nil {
return err
}
for _, profileID := range allowed {
if _, err := tx.Exec(`INSERT INTO personal_collection_profiles (collection_id, profile_id) VALUES (?, ?)`, input.ID, profileID); err != nil {
return err
}
}
}
return tx.Commit()
}
// DeleteCollection removes a collection and all of its items.
func DeleteCollection(db *sql.DB, id string) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.Exec(`DELETE FROM personal_collection_items WHERE collection_id = ?`, id); err != nil {
return err
}
if _, err := tx.Exec(`DELETE FROM personal_collections WHERE id = ?`, id); err != nil {
return err
}
return tx.Commit()
}
// AddCollectionItem adds a media item to a collection at the given position.
// If the item already exists in the collection, the operation is a no-op.
func AddCollectionItem(db *sql.DB, collectionID, mediaItemID string, position int) error {
_, err := db.Exec(
`INSERT OR IGNORE INTO personal_collection_items (collection_id, media_item_id, position, added_at) VALUES (?, ?, ?, ?)`,
collectionID, mediaItemID, position, nowUTC(),
)
return err
}
// RemoveCollectionItem removes a media item from a collection.
func RemoveCollectionItem(db *sql.DB, collectionID, mediaItemID string) error {
_, err := db.Exec(
`DELETE FROM personal_collection_items WHERE collection_id = ? AND media_item_id = ?`,
collectionID, mediaItemID,
)
return err
}
// ListCollectionItems returns all items in a collection, ordered by position ascending.
func ListCollectionItems(db *sql.DB, collectionID string) ([]CollectionItem, error) {
rows, err := db.Query(
`SELECT collection_id, media_item_id, position, added_at FROM personal_collection_items
WHERE collection_id = ? ORDER BY position ASC`,
collectionID,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []CollectionItem
for rows.Next() {
var ci CollectionItem
if err := rows.Scan(&ci.CollectionID, &ci.MediaItemID, &ci.Position, &ci.AddedAt); err != nil {
return nil, err
}
items = append(items, ci)
}
return items, rows.Err()
}
func listCollectionProfiles(db *sql.DB, collectionID string) ([]string, error) {
rows, err := db.Query(
`SELECT profile_id FROM personal_collection_profiles WHERE collection_id = ? ORDER BY profile_id ASC`,
collectionID,
)
if err != nil {
return nil, err
}
defer rows.Close()
return scanCollectionProfiles(rows)
}
func listCollectionProfilesTx(tx *sql.Tx, collectionID string) ([]string, error) {
rows, err := tx.Query(
`SELECT profile_id FROM personal_collection_profiles WHERE collection_id = ? ORDER BY profile_id ASC`,
collectionID,
)
if err != nil {
return nil, err
}
defer rows.Close()
return scanCollectionProfiles(rows)
}
func scanCollectionProfiles(rows *sql.Rows) ([]string, error) {
var profiles []string
for rows.Next() {
var profileID string
if err := rows.Scan(&profileID); err != nil {
return nil, err
}
profiles = append(profiles, profileID)
}
return profiles, rows.Err()
}
func normalizeAllowedProfiles(creatorProfileID string, allowedProfiles []string, isShared bool) []string {
if !isShared {
return []string{creatorProfileID}
}
seen := map[string]struct{}{creatorProfileID: {}}
normalized := []string{creatorProfileID}
for _, profileID := range allowedProfiles {
if profileID == "" {
continue
}
if _, ok := seen[profileID]; ok {
continue
}
seen[profileID] = struct{}{}
normalized = append(normalized, profileID)
}
return normalized
}