mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-01 11:53:36 -04:00
implement Spaces
This commit is contained in:
@@ -17,9 +17,6 @@ pub struct Space {
|
||||
/// Unique identifier
|
||||
pub id: Uuid,
|
||||
|
||||
/// Library this space belongs to
|
||||
pub library_id: Uuid,
|
||||
|
||||
/// Human-friendly name (e.g., "All Devices", "Work Files")
|
||||
pub name: String,
|
||||
|
||||
@@ -39,11 +36,10 @@ pub struct Space {
|
||||
|
||||
impl Space {
|
||||
/// Create a new space
|
||||
pub fn new(library_id: Uuid, name: String, icon: String, color: String) -> Self {
|
||||
pub fn new(name: String, icon: String, color: String) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
library_id,
|
||||
name,
|
||||
icon,
|
||||
color,
|
||||
@@ -59,9 +55,8 @@ impl Space {
|
||||
}
|
||||
|
||||
/// Create a default "All Devices" space
|
||||
pub fn create_default(library_id: Uuid) -> Self {
|
||||
pub fn create_default() -> Self {
|
||||
Self::new(
|
||||
library_id,
|
||||
"All Devices".to_string(),
|
||||
"Planet".to_string(),
|
||||
"#3B82F6".to_string(),
|
||||
@@ -283,15 +278,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_space_creation() {
|
||||
let library_id = Uuid::new_v4();
|
||||
let space = Space::new(
|
||||
library_id,
|
||||
"Test Space".to_string(),
|
||||
"Folder".to_string(),
|
||||
"#FF0000".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(space.library_id, library_id);
|
||||
assert_eq!(space.name, "Test Space");
|
||||
assert_eq!(space.icon, "Folder");
|
||||
assert_eq!(space.color, "#FF0000");
|
||||
@@ -300,8 +292,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_default_space() {
|
||||
let library_id = Uuid::new_v4();
|
||||
let space = Space::create_default(library_id);
|
||||
let space = Space::create_default();
|
||||
|
||||
assert_eq!(space.name, "All Devices");
|
||||
assert_eq!(space.icon, "Planet");
|
||||
|
||||
@@ -10,7 +10,6 @@ pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub uuid: Uuid,
|
||||
pub library_id: i32,
|
||||
pub name: String,
|
||||
pub icon: String,
|
||||
pub color: String,
|
||||
@@ -50,7 +49,7 @@ impl Syncable for Model {
|
||||
}
|
||||
|
||||
fn exclude_fields() -> Option<&'static [&'static str]> {
|
||||
Some(&["id", "library_id"])
|
||||
Some(&["id"])
|
||||
}
|
||||
|
||||
fn sync_depends_on() -> &'static [&'static str] {
|
||||
|
||||
@@ -30,7 +30,6 @@ impl MigrationTrait for Migration {
|
||||
.not_null()
|
||||
.unique_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Spaces::LibraryId).integer().not_null())
|
||||
.col(ColumnDef::new(Spaces::Name).string().not_null())
|
||||
.col(ColumnDef::new(Spaces::Icon).string().not_null())
|
||||
.col(ColumnDef::new(Spaces::Color).string().not_null())
|
||||
@@ -159,17 +158,7 @@ impl MigrationTrait for Migration {
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Create indexes for better query performance
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_spaces_library_id")
|
||||
.table(Spaces::Table)
|
||||
.col(Spaces::LibraryId)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Create index for better query performance
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
@@ -246,7 +235,6 @@ enum Spaces {
|
||||
Table,
|
||||
Id,
|
||||
Uuid,
|
||||
LibraryId,
|
||||
Name,
|
||||
Icon,
|
||||
Color,
|
||||
|
||||
@@ -293,6 +293,9 @@ impl LibraryManager {
|
||||
// Now open the library (which will call ensure_device_registered for current device)
|
||||
let library = self.open_library(&library_path, context).await?;
|
||||
|
||||
// Create default space with Quick Access group
|
||||
self.create_default_space(&library).await?;
|
||||
|
||||
// Emit event
|
||||
self.event_bus.emit(Event::LibraryCreated {
|
||||
id: library.id(),
|
||||
@@ -354,6 +357,9 @@ impl LibraryManager {
|
||||
// Open the newly created library
|
||||
let library = self.open_library(&library_path, context.clone()).await?;
|
||||
|
||||
// Create default space with Quick Access group
|
||||
self.create_default_space(&library).await?;
|
||||
|
||||
// Emit event
|
||||
self.event_bus.emit(Event::LibraryCreated {
|
||||
id: library.id(),
|
||||
@@ -940,6 +946,89 @@ impl LibraryManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create default space with Quick Access group for new libraries
|
||||
async fn create_default_space(&self, library: &Arc<Library>) -> Result<()> {
|
||||
use crate::domain::{GroupType, ItemType, Space, SpaceGroup, SpaceItem};
|
||||
use chrono::Utc;
|
||||
use sea_orm::{ActiveModelTrait, Set};
|
||||
|
||||
let db = library.db().conn();
|
||||
|
||||
// Create default space
|
||||
let space_id = uuid::Uuid::new_v4();
|
||||
let now = Utc::now();
|
||||
|
||||
let space_model = crate::infra::db::entities::space::ActiveModel {
|
||||
id: Set(0),
|
||||
uuid: Set(space_id),
|
||||
name: Set("All Devices".to_string()),
|
||||
icon: Set("Planet".to_string()),
|
||||
color: Set("#3B82F6".to_string()),
|
||||
order: Set(0),
|
||||
created_at: Set(now.into()),
|
||||
updated_at: Set(now.into()),
|
||||
};
|
||||
|
||||
let space_result = space_model
|
||||
.insert(db)
|
||||
.await
|
||||
.map_err(LibraryError::DatabaseError)?;
|
||||
|
||||
info!("Created default space for library {}", library.id());
|
||||
|
||||
// Create Quick Access group
|
||||
let group_id = uuid::Uuid::new_v4();
|
||||
|
||||
let group_type_json = serde_json::to_string(&GroupType::QuickAccess)
|
||||
.map_err(|e| LibraryError::Other(format!("Failed to serialize group_type: {}", e)))?;
|
||||
|
||||
let group_model = crate::infra::db::entities::space_group::ActiveModel {
|
||||
id: Set(0),
|
||||
uuid: Set(group_id),
|
||||
space_id: Set(space_result.id),
|
||||
name: Set("Quick Access".to_string()),
|
||||
group_type: Set(group_type_json),
|
||||
is_collapsed: Set(false),
|
||||
order: Set(0),
|
||||
created_at: Set(now.into()),
|
||||
};
|
||||
|
||||
let group_result = group_model
|
||||
.insert(db)
|
||||
.await
|
||||
.map_err(LibraryError::DatabaseError)?;
|
||||
|
||||
// Create Quick Access items (Overview, Recents, Favorites)
|
||||
let items = vec![
|
||||
(ItemType::Overview, 0),
|
||||
(ItemType::Recents, 1),
|
||||
(ItemType::Favorites, 2),
|
||||
];
|
||||
|
||||
for (item_type, order) in items {
|
||||
let item_type_json = serde_json::to_string(&item_type)
|
||||
.map_err(|e| LibraryError::Other(format!("Failed to serialize item_type: {}", e)))?;
|
||||
|
||||
let item_model = crate::infra::db::entities::space_item::ActiveModel {
|
||||
id: Set(0),
|
||||
uuid: Set(uuid::Uuid::new_v4()),
|
||||
group_id: Set(group_result.id),
|
||||
item_type: Set(item_type_json),
|
||||
order: Set(order),
|
||||
created_at: Set(now.into()),
|
||||
};
|
||||
|
||||
item_model
|
||||
.insert(db)
|
||||
.await
|
||||
.map_err(LibraryError::DatabaseError)?;
|
||||
}
|
||||
|
||||
info!("Created default Quick Access group for library {}", library.id());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if this device created the library (is the only device)
|
||||
async fn is_library_creator(&self, library: &Arc<Library>) -> Result<bool> {
|
||||
let db = library.db();
|
||||
|
||||
@@ -23,6 +23,7 @@ pub mod metadata;
|
||||
pub mod network;
|
||||
pub mod search;
|
||||
pub mod sidecar;
|
||||
pub mod spaces;
|
||||
pub mod sync;
|
||||
pub mod tags;
|
||||
pub mod volumes;
|
||||
|
||||
102
core/src/ops/spaces/add_group/action.rs
Normal file
102
core/src/ops/spaces/add_group/action.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use super::{input::AddGroupInput, output::AddGroupOutput};
|
||||
use crate::{
|
||||
context::CoreContext,
|
||||
domain::SpaceGroup,
|
||||
infra::action::{
|
||||
error::{ActionError, ActionResult},
|
||||
LibraryAction,
|
||||
},
|
||||
};
|
||||
use chrono::Utc;
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AddGroupAction {
|
||||
input: AddGroupInput,
|
||||
}
|
||||
|
||||
impl LibraryAction for AddGroupAction {
|
||||
type Input = AddGroupInput;
|
||||
type Output = AddGroupOutput;
|
||||
|
||||
fn from_input(input: AddGroupInput) -> Result<Self, String> {
|
||||
if input.name.trim().is_empty() {
|
||||
return Err("Group name cannot be empty".to_string());
|
||||
}
|
||||
|
||||
Ok(Self { input })
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
self,
|
||||
library: std::sync::Arc<crate::library::Library>,
|
||||
_context: std::sync::Arc<CoreContext>,
|
||||
) -> Result<Self::Output, ActionError> {
|
||||
let db = library.db().conn();
|
||||
|
||||
// Verify space exists
|
||||
let space_model = crate::infra::db::entities::space::Entity::find()
|
||||
.filter(crate::infra::db::entities::space::Column::Uuid.eq(self.input.space_id))
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(ActionError::SeaOrm)?
|
||||
.ok_or_else(|| ActionError::Internal(format!("Space {} not found", self.input.space_id)))?;
|
||||
|
||||
// Get max order for this space
|
||||
let max_order = crate::infra::db::entities::space_group::Entity::find()
|
||||
.filter(crate::infra::db::entities::space_group::Column::SpaceId.eq(space_model.id))
|
||||
.order_by_desc(crate::infra::db::entities::space_group::Column::Order)
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(ActionError::SeaOrm)?
|
||||
.map(|g| g.order)
|
||||
.unwrap_or(-1);
|
||||
|
||||
let group_id = uuid::Uuid::new_v4();
|
||||
let now = Utc::now();
|
||||
|
||||
// Serialize group_type to JSON
|
||||
let group_type_json = serde_json::to_string(&self.input.group_type)
|
||||
.map_err(|e| ActionError::Internal(format!("Failed to serialize group_type: {}", e)))?;
|
||||
|
||||
let active_model = crate::infra::db::entities::space_group::ActiveModel {
|
||||
id: Set(0),
|
||||
uuid: Set(group_id),
|
||||
space_id: Set(space_model.id),
|
||||
name: Set(self.input.name.clone()),
|
||||
group_type: Set(group_type_json),
|
||||
is_collapsed: Set(false),
|
||||
order: Set(max_order + 1),
|
||||
created_at: Set(now.into()),
|
||||
};
|
||||
|
||||
let result = active_model.insert(db).await.map_err(ActionError::SeaOrm)?;
|
||||
|
||||
let group = SpaceGroup {
|
||||
id: result.uuid,
|
||||
space_id: self.input.space_id,
|
||||
name: result.name,
|
||||
group_type: self.input.group_type,
|
||||
is_collapsed: result.is_collapsed,
|
||||
order: result.order,
|
||||
created_at: result.created_at.into(),
|
||||
};
|
||||
|
||||
Ok(AddGroupOutput { group })
|
||||
}
|
||||
|
||||
fn action_kind(&self) -> &'static str {
|
||||
"spaces.add_group"
|
||||
}
|
||||
|
||||
async fn validate(
|
||||
&self,
|
||||
_library: std::sync::Arc<crate::library::Library>,
|
||||
_context: std::sync::Arc<CoreContext>,
|
||||
) -> ActionResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
crate::register_library_action!(AddGroupAction, "spaces.add_group");
|
||||
11
core/src/ops/spaces/add_group/input.rs
Normal file
11
core/src/ops/spaces/add_group/input.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use crate::domain::GroupType;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct AddGroupInput {
|
||||
pub space_id: Uuid,
|
||||
pub name: String,
|
||||
pub group_type: GroupType,
|
||||
}
|
||||
7
core/src/ops/spaces/add_group/mod.rs
Normal file
7
core/src/ops/spaces/add_group/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod action;
|
||||
pub mod input;
|
||||
pub mod output;
|
||||
|
||||
pub use action::*;
|
||||
pub use input::*;
|
||||
pub use output::*;
|
||||
8
core/src/ops/spaces/add_group/output.rs
Normal file
8
core/src/ops/spaces/add_group/output.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use crate::domain::SpaceGroup;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct AddGroupOutput {
|
||||
pub group: SpaceGroup,
|
||||
}
|
||||
94
core/src/ops/spaces/add_item/action.rs
Normal file
94
core/src/ops/spaces/add_item/action.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use super::{input::AddItemInput, output::AddItemOutput};
|
||||
use crate::{
|
||||
context::CoreContext,
|
||||
domain::SpaceItem,
|
||||
infra::action::{
|
||||
error::{ActionError, ActionResult},
|
||||
LibraryAction,
|
||||
},
|
||||
};
|
||||
use chrono::Utc;
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AddItemAction {
|
||||
input: AddItemInput,
|
||||
}
|
||||
|
||||
impl LibraryAction for AddItemAction {
|
||||
type Input = AddItemInput;
|
||||
type Output = AddItemOutput;
|
||||
|
||||
fn from_input(input: AddItemInput) -> Result<Self, String> {
|
||||
Ok(Self { input })
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
self,
|
||||
library: std::sync::Arc<crate::library::Library>,
|
||||
_context: std::sync::Arc<CoreContext>,
|
||||
) -> Result<Self::Output, ActionError> {
|
||||
let db = library.db().conn();
|
||||
|
||||
// Verify group exists
|
||||
let group_model = crate::infra::db::entities::space_group::Entity::find()
|
||||
.filter(crate::infra::db::entities::space_group::Column::Uuid.eq(self.input.group_id))
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(ActionError::SeaOrm)?
|
||||
.ok_or_else(|| ActionError::Internal(format!("Group {} not found", self.input.group_id)))?;
|
||||
|
||||
// Get max order for this group
|
||||
let max_order = crate::infra::db::entities::space_item::Entity::find()
|
||||
.filter(crate::infra::db::entities::space_item::Column::GroupId.eq(group_model.id))
|
||||
.order_by_desc(crate::infra::db::entities::space_item::Column::Order)
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(ActionError::SeaOrm)?
|
||||
.map(|i| i.order)
|
||||
.unwrap_or(-1);
|
||||
|
||||
let item_id = uuid::Uuid::new_v4();
|
||||
let now = Utc::now();
|
||||
|
||||
// Serialize item_type to JSON
|
||||
let item_type_json = serde_json::to_string(&self.input.item_type)
|
||||
.map_err(|e| ActionError::Internal(format!("Failed to serialize item_type: {}", e)))?;
|
||||
|
||||
let active_model = crate::infra::db::entities::space_item::ActiveModel {
|
||||
id: Set(0),
|
||||
uuid: Set(item_id),
|
||||
group_id: Set(group_model.id),
|
||||
item_type: Set(item_type_json),
|
||||
order: Set(max_order + 1),
|
||||
created_at: Set(now.into()),
|
||||
};
|
||||
|
||||
let result = active_model.insert(db).await.map_err(ActionError::SeaOrm)?;
|
||||
|
||||
let item = SpaceItem {
|
||||
id: result.uuid,
|
||||
group_id: self.input.group_id,
|
||||
item_type: self.input.item_type,
|
||||
order: result.order,
|
||||
created_at: result.created_at.into(),
|
||||
};
|
||||
|
||||
Ok(AddItemOutput { item })
|
||||
}
|
||||
|
||||
fn action_kind(&self) -> &'static str {
|
||||
"spaces.add_item"
|
||||
}
|
||||
|
||||
async fn validate(
|
||||
&self,
|
||||
_library: std::sync::Arc<crate::library::Library>,
|
||||
_context: std::sync::Arc<CoreContext>,
|
||||
) -> ActionResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
crate::register_library_action!(AddItemAction, "spaces.add_item");
|
||||
10
core/src/ops/spaces/add_item/input.rs
Normal file
10
core/src/ops/spaces/add_item/input.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use crate::domain::ItemType;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct AddItemInput {
|
||||
pub group_id: Uuid,
|
||||
pub item_type: ItemType,
|
||||
}
|
||||
7
core/src/ops/spaces/add_item/mod.rs
Normal file
7
core/src/ops/spaces/add_item/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod action;
|
||||
pub mod input;
|
||||
pub mod output;
|
||||
|
||||
pub use action::*;
|
||||
pub use input::*;
|
||||
pub use output::*;
|
||||
8
core/src/ops/spaces/add_item/output.rs
Normal file
8
core/src/ops/spaces/add_item/output.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use crate::domain::SpaceItem;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct AddItemOutput {
|
||||
pub item: SpaceItem,
|
||||
}
|
||||
101
core/src/ops/spaces/create/action.rs
Normal file
101
core/src/ops/spaces/create/action.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use super::{input::SpaceCreateInput, output::SpaceCreateOutput};
|
||||
use crate::{
|
||||
context::CoreContext,
|
||||
domain::Space,
|
||||
infra::action::{
|
||||
error::{ActionError, ActionResult},
|
||||
LibraryAction,
|
||||
},
|
||||
};
|
||||
use chrono::Utc;
|
||||
use sea_orm::{ActiveModelTrait, EntityTrait, QueryOrder, Set};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SpaceCreateAction {
|
||||
input: SpaceCreateInput,
|
||||
}
|
||||
|
||||
impl SpaceCreateAction {
|
||||
pub fn new(input: SpaceCreateInput) -> Self {
|
||||
Self { input }
|
||||
}
|
||||
}
|
||||
|
||||
impl LibraryAction for SpaceCreateAction {
|
||||
type Input = SpaceCreateInput;
|
||||
type Output = SpaceCreateOutput;
|
||||
|
||||
fn from_input(input: SpaceCreateInput) -> Result<Self, String> {
|
||||
// Validate input
|
||||
if input.name.trim().is_empty() {
|
||||
return Err("Space name cannot be empty".to_string());
|
||||
}
|
||||
|
||||
if !Space::validate_color(&input.color) {
|
||||
return Err("Invalid color format. Must be #RRGGBB".to_string());
|
||||
}
|
||||
|
||||
Ok(SpaceCreateAction::new(input))
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
self,
|
||||
library: std::sync::Arc<crate::library::Library>,
|
||||
_context: std::sync::Arc<CoreContext>,
|
||||
) -> Result<Self::Output, ActionError> {
|
||||
let db = library.db().conn();
|
||||
|
||||
// Get the current max order
|
||||
let max_order = crate::infra::db::entities::space::Entity::find()
|
||||
.order_by_desc(crate::infra::db::entities::space::Column::Order)
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(ActionError::SeaOrm)?
|
||||
.map(|s| s.order)
|
||||
.unwrap_or(-1);
|
||||
|
||||
let space_id = uuid::Uuid::new_v4();
|
||||
let now = Utc::now();
|
||||
|
||||
// Create space entity
|
||||
let active_model = crate::infra::db::entities::space::ActiveModel {
|
||||
id: Set(0), // Auto-increment
|
||||
uuid: Set(space_id),
|
||||
name: Set(self.input.name.clone()),
|
||||
icon: Set(self.input.icon.clone()),
|
||||
color: Set(self.input.color.clone()),
|
||||
order: Set(max_order + 1),
|
||||
created_at: Set(now.into()),
|
||||
updated_at: Set(now.into()),
|
||||
};
|
||||
|
||||
let result = active_model.insert(db).await.map_err(ActionError::SeaOrm)?;
|
||||
|
||||
let space = Space {
|
||||
id: result.uuid,
|
||||
name: result.name,
|
||||
icon: result.icon,
|
||||
color: result.color,
|
||||
order: result.order,
|
||||
created_at: result.created_at.into(),
|
||||
updated_at: result.updated_at.into(),
|
||||
};
|
||||
|
||||
Ok(SpaceCreateOutput { space })
|
||||
}
|
||||
|
||||
fn action_kind(&self) -> &'static str {
|
||||
"spaces.create"
|
||||
}
|
||||
|
||||
async fn validate(
|
||||
&self,
|
||||
_library: std::sync::Arc<crate::library::Library>,
|
||||
_context: std::sync::Arc<CoreContext>,
|
||||
) -> ActionResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
crate::register_library_action!(SpaceCreateAction, "spaces.create");
|
||||
9
core/src/ops/spaces/create/input.rs
Normal file
9
core/src/ops/spaces/create/input.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct SpaceCreateInput {
|
||||
pub name: String,
|
||||
pub icon: String,
|
||||
pub color: String,
|
||||
}
|
||||
7
core/src/ops/spaces/create/mod.rs
Normal file
7
core/src/ops/spaces/create/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod action;
|
||||
pub mod input;
|
||||
pub mod output;
|
||||
|
||||
pub use action::*;
|
||||
pub use input::*;
|
||||
pub use output::*;
|
||||
8
core/src/ops/spaces/create/output.rs
Normal file
8
core/src/ops/spaces/create/output.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use crate::domain::Space;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct SpaceCreateOutput {
|
||||
pub space: Space,
|
||||
}
|
||||
58
core/src/ops/spaces/delete/action.rs
Normal file
58
core/src/ops/spaces/delete/action.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use super::{input::SpaceDeleteInput, output::SpaceDeleteOutput};
|
||||
use crate::{
|
||||
context::CoreContext,
|
||||
infra::action::{
|
||||
error::{ActionError, ActionResult},
|
||||
LibraryAction,
|
||||
},
|
||||
};
|
||||
use sea_orm::{ColumnTrait, EntityTrait, ModelTrait, QueryFilter};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SpaceDeleteAction {
|
||||
input: SpaceDeleteInput,
|
||||
}
|
||||
|
||||
impl LibraryAction for SpaceDeleteAction {
|
||||
type Input = SpaceDeleteInput;
|
||||
type Output = SpaceDeleteOutput;
|
||||
|
||||
fn from_input(input: SpaceDeleteInput) -> Result<Self, String> {
|
||||
Ok(Self { input })
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
self,
|
||||
library: std::sync::Arc<crate::library::Library>,
|
||||
_context: std::sync::Arc<CoreContext>,
|
||||
) -> Result<Self::Output, ActionError> {
|
||||
let db = library.db().conn();
|
||||
|
||||
let space_model = crate::infra::db::entities::space::Entity::find()
|
||||
.filter(crate::infra::db::entities::space::Column::Uuid.eq(self.input.space_id))
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(ActionError::SeaOrm)?
|
||||
.ok_or_else(|| ActionError::Internal(format!("Space {} not found", self.input.space_id)))?;
|
||||
|
||||
// Delete will cascade to groups and items due to foreign key constraints
|
||||
space_model.delete(db).await.map_err(ActionError::SeaOrm)?;
|
||||
|
||||
Ok(SpaceDeleteOutput { success: true })
|
||||
}
|
||||
|
||||
fn action_kind(&self) -> &'static str {
|
||||
"spaces.delete"
|
||||
}
|
||||
|
||||
async fn validate(
|
||||
&self,
|
||||
_library: std::sync::Arc<crate::library::Library>,
|
||||
_context: std::sync::Arc<CoreContext>,
|
||||
) -> ActionResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
crate::register_library_action!(SpaceDeleteAction, "spaces.delete");
|
||||
8
core/src/ops/spaces/delete/input.rs
Normal file
8
core/src/ops/spaces/delete/input.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct SpaceDeleteInput {
|
||||
pub space_id: Uuid,
|
||||
}
|
||||
7
core/src/ops/spaces/delete/mod.rs
Normal file
7
core/src/ops/spaces/delete/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod action;
|
||||
pub mod input;
|
||||
pub mod output;
|
||||
|
||||
pub use action::*;
|
||||
pub use input::*;
|
||||
pub use output::*;
|
||||
7
core/src/ops/spaces/delete/output.rs
Normal file
7
core/src/ops/spaces/delete/output.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct SpaceDeleteOutput {
|
||||
pub success: bool,
|
||||
}
|
||||
58
core/src/ops/spaces/delete_group/action.rs
Normal file
58
core/src/ops/spaces/delete_group/action.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use super::{input::DeleteGroupInput, output::DeleteGroupOutput};
|
||||
use crate::{
|
||||
context::CoreContext,
|
||||
infra::action::{
|
||||
error::{ActionError, ActionResult},
|
||||
LibraryAction,
|
||||
},
|
||||
};
|
||||
use sea_orm::{ColumnTrait, EntityTrait, ModelTrait, QueryFilter};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeleteGroupAction {
|
||||
input: DeleteGroupInput,
|
||||
}
|
||||
|
||||
impl LibraryAction for DeleteGroupAction {
|
||||
type Input = DeleteGroupInput;
|
||||
type Output = DeleteGroupOutput;
|
||||
|
||||
fn from_input(input: DeleteGroupInput) -> Result<Self, String> {
|
||||
Ok(Self { input })
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
self,
|
||||
library: std::sync::Arc<crate::library::Library>,
|
||||
_context: std::sync::Arc<CoreContext>,
|
||||
) -> Result<Self::Output, ActionError> {
|
||||
let db = library.db().conn();
|
||||
|
||||
let group_model = crate::infra::db::entities::space_group::Entity::find()
|
||||
.filter(crate::infra::db::entities::space_group::Column::Uuid.eq(self.input.group_id))
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(ActionError::SeaOrm)?
|
||||
.ok_or_else(|| ActionError::Internal(format!("Group {} not found", self.input.group_id)))?;
|
||||
|
||||
// Delete will cascade to items due to foreign key constraints
|
||||
group_model.delete(db).await.map_err(ActionError::SeaOrm)?;
|
||||
|
||||
Ok(DeleteGroupOutput { success: true })
|
||||
}
|
||||
|
||||
fn action_kind(&self) -> &'static str {
|
||||
"spaces.delete_group"
|
||||
}
|
||||
|
||||
async fn validate(
|
||||
&self,
|
||||
_library: std::sync::Arc<crate::library::Library>,
|
||||
_context: std::sync::Arc<CoreContext>,
|
||||
) -> ActionResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
crate::register_library_action!(DeleteGroupAction, "spaces.delete_group");
|
||||
8
core/src/ops/spaces/delete_group/input.rs
Normal file
8
core/src/ops/spaces/delete_group/input.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct DeleteGroupInput {
|
||||
pub group_id: Uuid,
|
||||
}
|
||||
7
core/src/ops/spaces/delete_group/mod.rs
Normal file
7
core/src/ops/spaces/delete_group/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod action;
|
||||
pub mod input;
|
||||
pub mod output;
|
||||
|
||||
pub use action::*;
|
||||
pub use input::*;
|
||||
pub use output::*;
|
||||
7
core/src/ops/spaces/delete_group/output.rs
Normal file
7
core/src/ops/spaces/delete_group/output.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct DeleteGroupOutput {
|
||||
pub success: bool,
|
||||
}
|
||||
57
core/src/ops/spaces/delete_item/action.rs
Normal file
57
core/src/ops/spaces/delete_item/action.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use super::{input::DeleteItemInput, output::DeleteItemOutput};
|
||||
use crate::{
|
||||
context::CoreContext,
|
||||
infra::action::{
|
||||
error::{ActionError, ActionResult},
|
||||
LibraryAction,
|
||||
},
|
||||
};
|
||||
use sea_orm::{ColumnTrait, EntityTrait, ModelTrait, QueryFilter};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeleteItemAction {
|
||||
input: DeleteItemInput,
|
||||
}
|
||||
|
||||
impl LibraryAction for DeleteItemAction {
|
||||
type Input = DeleteItemInput;
|
||||
type Output = DeleteItemOutput;
|
||||
|
||||
fn from_input(input: DeleteItemInput) -> Result<Self, String> {
|
||||
Ok(Self { input })
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
self,
|
||||
library: std::sync::Arc<crate::library::Library>,
|
||||
_context: std::sync::Arc<CoreContext>,
|
||||
) -> Result<Self::Output, ActionError> {
|
||||
let db = library.db().conn();
|
||||
|
||||
let item_model = crate::infra::db::entities::space_item::Entity::find()
|
||||
.filter(crate::infra::db::entities::space_item::Column::Uuid.eq(self.input.item_id))
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(ActionError::SeaOrm)?
|
||||
.ok_or_else(|| ActionError::Internal(format!("Item {} not found", self.input.item_id)))?;
|
||||
|
||||
item_model.delete(db).await.map_err(ActionError::SeaOrm)?;
|
||||
|
||||
Ok(DeleteItemOutput { success: true })
|
||||
}
|
||||
|
||||
fn action_kind(&self) -> &'static str {
|
||||
"spaces.delete_item"
|
||||
}
|
||||
|
||||
async fn validate(
|
||||
&self,
|
||||
_library: std::sync::Arc<crate::library::Library>,
|
||||
_context: std::sync::Arc<CoreContext>,
|
||||
) -> ActionResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
crate::register_library_action!(DeleteItemAction, "spaces.delete_item");
|
||||
8
core/src/ops/spaces/delete_item/input.rs
Normal file
8
core/src/ops/spaces/delete_item/input.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct DeleteItemInput {
|
||||
pub item_id: Uuid,
|
||||
}
|
||||
7
core/src/ops/spaces/delete_item/mod.rs
Normal file
7
core/src/ops/spaces/delete_item/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod action;
|
||||
pub mod input;
|
||||
pub mod output;
|
||||
|
||||
pub use action::*;
|
||||
pub use input::*;
|
||||
pub use output::*;
|
||||
7
core/src/ops/spaces/delete_item/output.rs
Normal file
7
core/src/ops/spaces/delete_item/output.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct DeleteItemOutput {
|
||||
pub success: bool,
|
||||
}
|
||||
5
core/src/ops/spaces/get/mod.rs
Normal file
5
core/src/ops/spaces/get/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod output;
|
||||
pub mod query;
|
||||
|
||||
pub use output::*;
|
||||
pub use query::*;
|
||||
8
core/src/ops/spaces/get/output.rs
Normal file
8
core/src/ops/spaces/get/output.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use crate::domain::Space;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct SpaceGetOutput {
|
||||
pub space: Space,
|
||||
}
|
||||
69
core/src/ops/spaces/get/query.rs
Normal file
69
core/src/ops/spaces/get/query.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use super::output::SpaceGetOutput;
|
||||
use crate::domain::Space;
|
||||
use crate::infra::query::{QueryError, QueryResult};
|
||||
use crate::{context::CoreContext, infra::query::LibraryQuery};
|
||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct SpaceGetQueryInput {
|
||||
pub space_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct SpaceGetQuery {
|
||||
space_id: Uuid,
|
||||
}
|
||||
|
||||
impl LibraryQuery for SpaceGetQuery {
|
||||
type Input = SpaceGetQueryInput;
|
||||
type Output = SpaceGetOutput;
|
||||
|
||||
fn from_input(input: Self::Input) -> QueryResult<Self> {
|
||||
Ok(Self {
|
||||
space_id: input.space_id,
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
self,
|
||||
context: Arc<CoreContext>,
|
||||
session: crate::infra::api::SessionContext,
|
||||
) -> QueryResult<Self::Output> {
|
||||
let library_id = session
|
||||
.current_library_id
|
||||
.ok_or_else(|| QueryError::Internal("No library selected".to_string()))?;
|
||||
|
||||
let library = context
|
||||
.libraries()
|
||||
.await
|
||||
.get_library(library_id)
|
||||
.await
|
||||
.ok_or_else(|| QueryError::Internal("Library not found".to_string()))?;
|
||||
|
||||
let db = library.db().conn();
|
||||
|
||||
let space_model = crate::infra::db::entities::space::Entity::find()
|
||||
.filter(crate::infra::db::entities::space::Column::Uuid.eq(self.space_id))
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or_else(|| QueryError::Internal(format!("Space {} not found", self.space_id)))?;
|
||||
|
||||
let space = Space {
|
||||
id: space_model.uuid,
|
||||
name: space_model.name,
|
||||
icon: space_model.icon,
|
||||
color: space_model.color,
|
||||
order: space_model.order,
|
||||
created_at: space_model.created_at.into(),
|
||||
updated_at: space_model.updated_at.into(),
|
||||
};
|
||||
|
||||
Ok(SpaceGetOutput { space })
|
||||
}
|
||||
}
|
||||
|
||||
crate::register_library_query!(SpaceGetQuery, "spaces.get");
|
||||
5
core/src/ops/spaces/get_layout/mod.rs
Normal file
5
core/src/ops/spaces/get_layout/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod output;
|
||||
pub mod query;
|
||||
|
||||
pub use output::*;
|
||||
pub use query::*;
|
||||
8
core/src/ops/spaces/get_layout/output.rs
Normal file
8
core/src/ops/spaces/get_layout/output.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use crate::domain::{SpaceLayout};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct SpaceLayoutOutput {
|
||||
pub layout: SpaceLayout,
|
||||
}
|
||||
122
core/src/ops/spaces/get_layout/query.rs
Normal file
122
core/src/ops/spaces/get_layout/query.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use super::output::SpaceLayoutOutput;
|
||||
use crate::domain::{GroupType, ItemType, Space, SpaceGroup, SpaceGroupWithItems, SpaceItem, SpaceLayout};
|
||||
use crate::infra::query::{QueryError, QueryResult};
|
||||
use crate::{context::CoreContext, infra::query::LibraryQuery};
|
||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct SpaceLayoutQueryInput {
|
||||
pub space_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct SpaceLayoutQuery {
|
||||
space_id: Uuid,
|
||||
}
|
||||
|
||||
impl LibraryQuery for SpaceLayoutQuery {
|
||||
type Input = SpaceLayoutQueryInput;
|
||||
type Output = SpaceLayoutOutput;
|
||||
|
||||
fn from_input(input: Self::Input) -> QueryResult<Self> {
|
||||
Ok(Self {
|
||||
space_id: input.space_id,
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
self,
|
||||
context: Arc<CoreContext>,
|
||||
session: crate::infra::api::SessionContext,
|
||||
) -> QueryResult<Self::Output> {
|
||||
let library_id = session
|
||||
.current_library_id
|
||||
.ok_or_else(|| QueryError::Internal("No library selected".to_string()))?;
|
||||
|
||||
let library = context
|
||||
.libraries()
|
||||
.await
|
||||
.get_library(library_id)
|
||||
.await
|
||||
.ok_or_else(|| QueryError::Internal("Library not found".to_string()))?;
|
||||
|
||||
let db = library.db().conn();
|
||||
|
||||
// Get space
|
||||
let space_model = crate::infra::db::entities::space::Entity::find()
|
||||
.filter(crate::infra::db::entities::space::Column::Uuid.eq(self.space_id))
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or_else(|| QueryError::Internal(format!("Space {} not found", self.space_id)))?;
|
||||
|
||||
let space = Space {
|
||||
id: space_model.uuid,
|
||||
name: space_model.name,
|
||||
icon: space_model.icon,
|
||||
color: space_model.color,
|
||||
order: space_model.order,
|
||||
created_at: space_model.created_at.into(),
|
||||
updated_at: space_model.updated_at.into(),
|
||||
};
|
||||
|
||||
// Get groups for this space
|
||||
let group_models = crate::infra::db::entities::space_group::Entity::find()
|
||||
.filter(crate::infra::db::entities::space_group::Column::SpaceId.eq(space_model.id))
|
||||
.order_by_asc(crate::infra::db::entities::space_group::Column::Order)
|
||||
.all(db)
|
||||
.await?;
|
||||
|
||||
let mut groups = Vec::new();
|
||||
|
||||
for group_model in group_models {
|
||||
// Parse group_type JSON
|
||||
let group_type: GroupType = serde_json::from_str(&group_model.group_type)
|
||||
.map_err(|e| QueryError::Internal(format!("Failed to parse group_type: {}", e)))?;
|
||||
|
||||
let group = SpaceGroup {
|
||||
id: group_model.uuid,
|
||||
space_id: self.space_id,
|
||||
name: group_model.name,
|
||||
group_type,
|
||||
is_collapsed: group_model.is_collapsed,
|
||||
order: group_model.order,
|
||||
created_at: group_model.created_at.into(),
|
||||
};
|
||||
|
||||
// Get items for this group
|
||||
let item_models = crate::infra::db::entities::space_item::Entity::find()
|
||||
.filter(crate::infra::db::entities::space_item::Column::GroupId.eq(group_model.id))
|
||||
.order_by_asc(crate::infra::db::entities::space_item::Column::Order)
|
||||
.all(db)
|
||||
.await?;
|
||||
|
||||
let mut items = Vec::new();
|
||||
|
||||
for item_model in item_models {
|
||||
// Parse item_type JSON
|
||||
let item_type: ItemType = serde_json::from_str(&item_model.item_type)
|
||||
.map_err(|e| QueryError::Internal(format!("Failed to parse item_type: {}", e)))?;
|
||||
|
||||
items.push(SpaceItem {
|
||||
id: item_model.uuid,
|
||||
group_id: group_model.uuid,
|
||||
item_type,
|
||||
order: item_model.order,
|
||||
created_at: item_model.created_at.into(),
|
||||
});
|
||||
}
|
||||
|
||||
groups.push(SpaceGroupWithItems { group, items });
|
||||
}
|
||||
|
||||
let layout = SpaceLayout { space, groups };
|
||||
|
||||
Ok(SpaceLayoutOutput { layout })
|
||||
}
|
||||
}
|
||||
|
||||
crate::register_library_query!(SpaceLayoutQuery, "spaces.get_layout");
|
||||
5
core/src/ops/spaces/list/mod.rs
Normal file
5
core/src/ops/spaces/list/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod output;
|
||||
pub mod query;
|
||||
|
||||
pub use output::*;
|
||||
pub use query::*;
|
||||
8
core/src/ops/spaces/list/output.rs
Normal file
8
core/src/ops/spaces/list/output.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use crate::domain::{Space};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct SpacesListOutput {
|
||||
pub spaces: Vec<Space>,
|
||||
}
|
||||
64
core/src/ops/spaces/list/query.rs
Normal file
64
core/src/ops/spaces/list/query.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
use super::output::SpacesListOutput;
|
||||
use crate::domain::Space;
|
||||
use crate::infra::query::{QueryError, QueryResult};
|
||||
use crate::{context::CoreContext, infra::query::LibraryQuery};
|
||||
use sea_orm::{EntityTrait, QueryOrder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct SpacesListQueryInput;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct SpacesListQuery;
|
||||
|
||||
impl LibraryQuery for SpacesListQuery {
|
||||
type Input = SpacesListQueryInput;
|
||||
type Output = SpacesListOutput;
|
||||
|
||||
fn from_input(_input: Self::Input) -> QueryResult<Self> {
|
||||
Ok(Self {})
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
self,
|
||||
context: Arc<CoreContext>,
|
||||
session: crate::infra::api::SessionContext,
|
||||
) -> QueryResult<Self::Output> {
|
||||
let library_id = session
|
||||
.current_library_id
|
||||
.ok_or_else(|| QueryError::Internal("No library selected".to_string()))?;
|
||||
|
||||
let library = context
|
||||
.libraries()
|
||||
.await
|
||||
.get_library(library_id)
|
||||
.await
|
||||
.ok_or_else(|| QueryError::Internal("Library not found".to_string()))?;
|
||||
|
||||
let db = library.db().conn();
|
||||
|
||||
let space_models = crate::infra::db::entities::space::Entity::find()
|
||||
.order_by_asc(crate::infra::db::entities::space::Column::Order)
|
||||
.all(db)
|
||||
.await?;
|
||||
|
||||
let spaces = space_models
|
||||
.into_iter()
|
||||
.map(|model| Space {
|
||||
id: model.uuid,
|
||||
name: model.name,
|
||||
icon: model.icon,
|
||||
color: model.color,
|
||||
order: model.order,
|
||||
created_at: model.created_at.into(),
|
||||
updated_at: model.updated_at.into(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(SpacesListOutput { spaces })
|
||||
}
|
||||
}
|
||||
|
||||
crate::register_library_query!(SpacesListQuery, "spaces.list");
|
||||
29
core/src/ops/spaces/mod.rs
Normal file
29
core/src/ops/spaces/mod.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
//! Space operations
|
||||
//!
|
||||
//! Queries and actions for managing Spaces, SpaceGroups, and SpaceItems
|
||||
|
||||
pub mod add_group;
|
||||
pub mod add_item;
|
||||
pub mod create;
|
||||
pub mod delete;
|
||||
pub mod delete_group;
|
||||
pub mod delete_item;
|
||||
pub mod get;
|
||||
pub mod get_layout;
|
||||
pub mod list;
|
||||
pub mod reorder;
|
||||
pub mod update;
|
||||
pub mod update_group;
|
||||
|
||||
pub use add_group::*;
|
||||
pub use add_item::*;
|
||||
pub use create::*;
|
||||
pub use delete::*;
|
||||
pub use delete_group::*;
|
||||
pub use delete_item::*;
|
||||
pub use get::*;
|
||||
pub use get_layout::*;
|
||||
pub use list::*;
|
||||
pub use reorder::*;
|
||||
pub use update::*;
|
||||
pub use update_group::*;
|
||||
118
core/src/ops/spaces/reorder/action.rs
Normal file
118
core/src/ops/spaces/reorder/action.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
use super::{
|
||||
input::{ReorderGroupsInput, ReorderItemsInput},
|
||||
output::ReorderOutput,
|
||||
};
|
||||
use crate::{
|
||||
context::CoreContext,
|
||||
infra::action::{
|
||||
error::{ActionError, ActionResult},
|
||||
LibraryAction,
|
||||
},
|
||||
};
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ReorderGroupsAction {
|
||||
input: ReorderGroupsInput,
|
||||
}
|
||||
|
||||
impl LibraryAction for ReorderGroupsAction {
|
||||
type Input = ReorderGroupsInput;
|
||||
type Output = ReorderOutput;
|
||||
|
||||
fn from_input(input: ReorderGroupsInput) -> Result<Self, String> {
|
||||
Ok(Self { input })
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
self,
|
||||
library: std::sync::Arc<crate::library::Library>,
|
||||
_context: std::sync::Arc<CoreContext>,
|
||||
) -> Result<Self::Output, ActionError> {
|
||||
let db = library.db().conn();
|
||||
|
||||
// Update order for each group
|
||||
for (index, group_id) in self.input.group_ids.iter().enumerate() {
|
||||
let group_model = crate::infra::db::entities::space_group::Entity::find()
|
||||
.filter(crate::infra::db::entities::space_group::Column::Uuid.eq(*group_id))
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(ActionError::SeaOrm)?
|
||||
.ok_or_else(|| ActionError::Internal(format!("Group {} not found", group_id)))?;
|
||||
|
||||
let mut active_model: crate::infra::db::entities::space_group::ActiveModel =
|
||||
group_model.into();
|
||||
active_model.order = Set(index as i32);
|
||||
active_model.update(db).await.map_err(ActionError::SeaOrm)?;
|
||||
}
|
||||
|
||||
Ok(ReorderOutput { success: true })
|
||||
}
|
||||
|
||||
fn action_kind(&self) -> &'static str {
|
||||
"spaces.reorder_groups"
|
||||
}
|
||||
|
||||
async fn validate(
|
||||
&self,
|
||||
_library: std::sync::Arc<crate::library::Library>,
|
||||
_context: std::sync::Arc<CoreContext>,
|
||||
) -> ActionResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ReorderItemsAction {
|
||||
input: ReorderItemsInput,
|
||||
}
|
||||
|
||||
impl LibraryAction for ReorderItemsAction {
|
||||
type Input = ReorderItemsInput;
|
||||
type Output = ReorderOutput;
|
||||
|
||||
fn from_input(input: ReorderItemsInput) -> Result<Self, String> {
|
||||
Ok(Self { input })
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
self,
|
||||
library: std::sync::Arc<crate::library::Library>,
|
||||
_context: std::sync::Arc<CoreContext>,
|
||||
) -> Result<Self::Output, ActionError> {
|
||||
let db = library.db().conn();
|
||||
|
||||
// Update order for each item
|
||||
for (index, item_id) in self.input.item_ids.iter().enumerate() {
|
||||
let item_model = crate::infra::db::entities::space_item::Entity::find()
|
||||
.filter(crate::infra::db::entities::space_item::Column::Uuid.eq(*item_id))
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(ActionError::SeaOrm)?
|
||||
.ok_or_else(|| ActionError::Internal(format!("Item {} not found", item_id)))?;
|
||||
|
||||
let mut active_model: crate::infra::db::entities::space_item::ActiveModel =
|
||||
item_model.into();
|
||||
active_model.order = Set(index as i32);
|
||||
active_model.update(db).await.map_err(ActionError::SeaOrm)?;
|
||||
}
|
||||
|
||||
Ok(ReorderOutput { success: true })
|
||||
}
|
||||
|
||||
fn action_kind(&self) -> &'static str {
|
||||
"spaces.reorder_items"
|
||||
}
|
||||
|
||||
async fn validate(
|
||||
&self,
|
||||
_library: std::sync::Arc<crate::library::Library>,
|
||||
_context: std::sync::Arc<CoreContext>,
|
||||
) -> ActionResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
crate::register_library_action!(ReorderGroupsAction, "spaces.reorder_groups");
|
||||
crate::register_library_action!(ReorderItemsAction, "spaces.reorder_items");
|
||||
15
core/src/ops/spaces/reorder/input.rs
Normal file
15
core/src/ops/spaces/reorder/input.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct ReorderGroupsInput {
|
||||
pub space_id: Uuid,
|
||||
pub group_ids: Vec<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct ReorderItemsInput {
|
||||
pub group_id: Uuid,
|
||||
pub item_ids: Vec<Uuid>,
|
||||
}
|
||||
7
core/src/ops/spaces/reorder/mod.rs
Normal file
7
core/src/ops/spaces/reorder/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod action;
|
||||
pub mod input;
|
||||
pub mod output;
|
||||
|
||||
pub use action::*;
|
||||
pub use input::*;
|
||||
pub use output::*;
|
||||
7
core/src/ops/spaces/reorder/output.rs
Normal file
7
core/src/ops/spaces/reorder/output.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct ReorderOutput {
|
||||
pub success: bool,
|
||||
}
|
||||
97
core/src/ops/spaces/update/action.rs
Normal file
97
core/src/ops/spaces/update/action.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
use super::{input::SpaceUpdateInput, output::SpaceUpdateOutput};
|
||||
use crate::{
|
||||
context::CoreContext,
|
||||
domain::Space,
|
||||
infra::action::{
|
||||
error::{ActionError, ActionResult},
|
||||
LibraryAction,
|
||||
},
|
||||
};
|
||||
use chrono::Utc;
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SpaceUpdateAction {
|
||||
input: SpaceUpdateInput,
|
||||
}
|
||||
|
||||
impl LibraryAction for SpaceUpdateAction {
|
||||
type Input = SpaceUpdateInput;
|
||||
type Output = SpaceUpdateOutput;
|
||||
|
||||
fn from_input(input: SpaceUpdateInput) -> Result<Self, String> {
|
||||
if let Some(ref name) = input.name {
|
||||
if name.trim().is_empty() {
|
||||
return Err("Space name cannot be empty".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref color) = input.color {
|
||||
if !Space::validate_color(color) {
|
||||
return Err("Invalid color format. Must be #RRGGBB".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self { input })
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
self,
|
||||
library: std::sync::Arc<crate::library::Library>,
|
||||
_context: std::sync::Arc<CoreContext>,
|
||||
) -> Result<Self::Output, ActionError> {
|
||||
let db = library.db().conn();
|
||||
|
||||
let space_model = crate::infra::db::entities::space::Entity::find()
|
||||
.filter(crate::infra::db::entities::space::Column::Uuid.eq(self.input.space_id))
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(ActionError::SeaOrm)?
|
||||
.ok_or_else(|| ActionError::Internal(format!("Space {} not found", self.input.space_id)))?;
|
||||
|
||||
let mut active_model: crate::infra::db::entities::space::ActiveModel = space_model.into();
|
||||
|
||||
if let Some(name) = self.input.name {
|
||||
active_model.name = Set(name);
|
||||
}
|
||||
|
||||
if let Some(icon) = self.input.icon {
|
||||
active_model.icon = Set(icon);
|
||||
}
|
||||
|
||||
if let Some(color) = self.input.color {
|
||||
active_model.color = Set(color);
|
||||
}
|
||||
|
||||
active_model.updated_at = Set(Utc::now().into());
|
||||
|
||||
let result = active_model.update(db).await.map_err(ActionError::SeaOrm)?;
|
||||
|
||||
let space = Space {
|
||||
id: result.uuid,
|
||||
name: result.name,
|
||||
icon: result.icon,
|
||||
color: result.color,
|
||||
order: result.order,
|
||||
created_at: result.created_at.into(),
|
||||
updated_at: result.updated_at.into(),
|
||||
};
|
||||
|
||||
Ok(SpaceUpdateOutput { space })
|
||||
}
|
||||
|
||||
fn action_kind(&self) -> &'static str {
|
||||
"spaces.update"
|
||||
}
|
||||
|
||||
async fn validate(
|
||||
&self,
|
||||
_library: std::sync::Arc<crate::library::Library>,
|
||||
_context: std::sync::Arc<CoreContext>,
|
||||
) -> ActionResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
crate::register_library_action!(SpaceUpdateAction, "spaces.update");
|
||||
11
core/src/ops/spaces/update/input.rs
Normal file
11
core/src/ops/spaces/update/input.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct SpaceUpdateInput {
|
||||
pub space_id: Uuid,
|
||||
pub name: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub color: Option<String>,
|
||||
}
|
||||
7
core/src/ops/spaces/update/mod.rs
Normal file
7
core/src/ops/spaces/update/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod action;
|
||||
pub mod input;
|
||||
pub mod output;
|
||||
|
||||
pub use action::*;
|
||||
pub use input::*;
|
||||
pub use output::*;
|
||||
8
core/src/ops/spaces/update/output.rs
Normal file
8
core/src/ops/spaces/update/output.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use crate::domain::Space;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct SpaceUpdateOutput {
|
||||
pub space: Space,
|
||||
}
|
||||
95
core/src/ops/spaces/update_group/action.rs
Normal file
95
core/src/ops/spaces/update_group/action.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use super::{input::UpdateGroupInput, output::UpdateGroupOutput};
|
||||
use crate::{
|
||||
context::CoreContext,
|
||||
domain::{GroupType, SpaceGroup},
|
||||
infra::action::{
|
||||
error::{ActionError, ActionResult},
|
||||
LibraryAction,
|
||||
},
|
||||
};
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateGroupAction {
|
||||
input: UpdateGroupInput,
|
||||
}
|
||||
|
||||
impl LibraryAction for UpdateGroupAction {
|
||||
type Input = UpdateGroupInput;
|
||||
type Output = UpdateGroupOutput;
|
||||
|
||||
fn from_input(input: UpdateGroupInput) -> Result<Self, String> {
|
||||
if let Some(ref name) = input.name {
|
||||
if name.trim().is_empty() {
|
||||
return Err("Group name cannot be empty".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self { input })
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
self,
|
||||
library: std::sync::Arc<crate::library::Library>,
|
||||
_context: std::sync::Arc<CoreContext>,
|
||||
) -> Result<Self::Output, ActionError> {
|
||||
let db = library.db().conn();
|
||||
|
||||
let group_model = crate::infra::db::entities::space_group::Entity::find()
|
||||
.filter(crate::infra::db::entities::space_group::Column::Uuid.eq(self.input.group_id))
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(ActionError::SeaOrm)?
|
||||
.ok_or_else(|| ActionError::Internal(format!("Group {} not found", self.input.group_id)))?;
|
||||
|
||||
let mut active_model: crate::infra::db::entities::space_group::ActiveModel = group_model.clone().into();
|
||||
|
||||
if let Some(name) = self.input.name {
|
||||
active_model.name = Set(name);
|
||||
}
|
||||
|
||||
if let Some(is_collapsed) = self.input.is_collapsed {
|
||||
active_model.is_collapsed = Set(is_collapsed);
|
||||
}
|
||||
|
||||
let result = active_model.update(db).await.map_err(ActionError::SeaOrm)?;
|
||||
|
||||
// Parse group_type from JSON
|
||||
let group_type: GroupType = serde_json::from_str(&result.group_type)
|
||||
.map_err(|e| ActionError::Internal(format!("Failed to parse group_type: {}", e)))?;
|
||||
|
||||
// Get space UUID
|
||||
let space_model = crate::infra::db::entities::space::Entity::find_by_id(result.space_id)
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(ActionError::SeaOrm)?
|
||||
.ok_or_else(|| ActionError::Internal("Space not found for group".to_string()))?;
|
||||
|
||||
let group = SpaceGroup {
|
||||
id: result.uuid,
|
||||
space_id: space_model.uuid,
|
||||
name: result.name,
|
||||
group_type,
|
||||
is_collapsed: result.is_collapsed,
|
||||
order: result.order,
|
||||
created_at: result.created_at.into(),
|
||||
};
|
||||
|
||||
Ok(UpdateGroupOutput { group })
|
||||
}
|
||||
|
||||
fn action_kind(&self) -> &'static str {
|
||||
"spaces.update_group"
|
||||
}
|
||||
|
||||
async fn validate(
|
||||
&self,
|
||||
_library: std::sync::Arc<crate::library::Library>,
|
||||
_context: std::sync::Arc<CoreContext>,
|
||||
) -> ActionResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
crate::register_library_action!(UpdateGroupAction, "spaces.update_group");
|
||||
10
core/src/ops/spaces/update_group/input.rs
Normal file
10
core/src/ops/spaces/update_group/input.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct UpdateGroupInput {
|
||||
pub group_id: Uuid,
|
||||
pub name: Option<String>,
|
||||
pub is_collapsed: Option<bool>,
|
||||
}
|
||||
7
core/src/ops/spaces/update_group/mod.rs
Normal file
7
core/src/ops/spaces/update_group/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod action;
|
||||
pub mod input;
|
||||
pub mod output;
|
||||
|
||||
pub use action::*;
|
||||
pub use input::*;
|
||||
pub use output::*;
|
||||
8
core/src/ops/spaces/update_group/output.rs
Normal file
8
core/src/ops/spaces/update_group/output.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use crate::domain::SpaceGroup;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct UpdateGroupOutput {
|
||||
pub group: SpaceGroup,
|
||||
}
|
||||
Reference in New Issue
Block a user