selene-core 0.9.0-rc.1

Backend for selene-server
Documentation
use chrono::Utc;
use lunar_lib::{
    database::{
        CompareAndSwapTransaction, Createable, CustomTransactionError, DatabaseEntry, Db, DbIdExt,
        Deleteable, Entry, TransactionError, caching::Cacheable,
    },
    id::Id,
    iterator_ext::IteratorExtensions,
};

use crate::{
    accounts::account::Account,
    database::Selene,
    library::{
        collectable::Collectable,
        collection::{Collection, CollectionCreationError},
    },
};

#[derive(Debug, Clone)]
pub struct CollectionCreateArgs {
    name: String,
    items: Vec<Collectable>,
    account: Id<Account>,
    public: bool,
}

impl CollectionCreateArgs {
    pub fn new(name: impl Into<String>, user: Id<Account>, public: bool) -> Self {
        Self {
            name: name.into(),
            items: Vec::new(),
            account: user,
            public,
        }
    }

    #[must_use]
    pub fn with_items(mut self, items: Vec<Collectable>) -> Self {
        self.items = items;
        self
    }
}

impl DatabaseEntry for Collection {
    type DbInner = Selene;

    const VERSION_NUMBER: u32 = 1;

    const TREE_NAME: &str = "collections";
}

impl Cacheable for Collection {}

pub static COLLECTION_OWNER_INDEX: &str = "collection_owner_map";

impl Createable for Collection {
    type CreateArgs = CollectionCreateArgs;

    type Err = CollectionCreationError;

    fn create(
        CollectionCreateArgs {
            name,
            items,
            account,
            public,
        }: Self::CreateArgs,
        cas_tx: &mut CompareAndSwapTransaction<Self::DbInner>,
    ) -> Result<Entry<Self>, CustomTransactionError<Self::Err>> {
        let len = name.chars().take(33).count();
        if !(3..=32).contains(&len) {
            return Err(CustomTransactionError::Closure(
                CollectionCreationError::InvalidNameLength(len as u32),
            ));
        }

        let id = loop {
            let id = Id::generate();
            if !id.tx_check(cas_tx)? {
                break id;
            }
        };

        let now = Utc::now();
        let collection = Collection {
            name,
            owner: account,
            cover_art: None,
            items,
            public,
            created: now,
            modified: now,
        };

        let index = cas_tx.get_or_new_index(COLLECTION_OWNER_INDEX);
        let mut key = [0u8; 64];
        key[0..32].copy_from_slice(&*account);
        key[32..64].copy_from_slice(&*id);
        index.upsert(key.as_slice(), &[u8::from(public)])?;

        Ok(collection.to_entry(id))
    }
}

impl Deleteable for Collection {
    fn unlink_references(
        old: Entry<Self>,
        cas_tx: &mut CompareAndSwapTransaction<Self::DbInner>,
    ) -> Result<(), TransactionError> {
        let index = cas_tx.get_or_new_index(COLLECTION_OWNER_INDEX);
        let mut key = [0u8; 64];
        key[0..32].copy_from_slice(&*old.owner);
        key[32..64].copy_from_slice(&*old.id());
        index.delete(key.as_slice())?;
        Ok(())
    }
}

impl Collection {
    pub fn get_collections_by_user(
        user: Id<Account>,
        db: &Db<Selene>,
    ) -> Result<Vec<(Id<Collection>, bool)>, TransactionError> {
        let index = db.index(COLLECTION_OWNER_INDEX);
        index
            .scan_prefix(user)
            .map(|r| {
                let (key, value) = r?;

                Ok((
                    Id::<Collection>::try_from(&key[32..64]).expect("IDs are 32 bytes"),
                    value[0] != 0,
                ))
            })
            .try_to_vec()
    }
}