use std::sync::mpsc::SyncSender; use chrono::Utc; use galaxy_graphql::scalars::time::ServerTimestamp; use galaxyui::{AppContext, Entity, ModelContext, SingletonEntity}; use crate::ai::execution_profiles::{ AIExecutionProfile, CloudAIExecutionProfile, CloudAIExecutionProfileModel, }; use crate::ai::facts::{AIFact, CloudAIFact, CloudAIFactModel}; use crate::ai::mcp::templatable::{ CloudTemplatableMCPServer, CloudTemplatableMCPServerModel, TemplatableMCPServer, }; use crate::auth::UserUid; use crate::cloud_object::model::generic_string_model::GenericStringObjectId; use crate::cloud_object::model::persistence::{CloudModel, CloudModelEvent}; use crate::cloud_object::{ CloudObject, CloudObjectLocation, CloudObjectMetadata, CloudObjectPermissions, CloudObjectStatuses, CloudObjectSyncStatus, GenericCloudObject, GenericStringObjectFormat, JsonObjectType, ObjectIdType, Owner, Revision, Space, }; use crate::drive::folders::{CloudFolder, CloudFolderModel}; use crate::drive::CloudObjectTypeAndId; use crate::env_vars::{CloudEnvVarCollection, CloudEnvVarCollectionModel, EnvVarCollection}; use crate::notebooks::{CloudNotebook, CloudNotebookModel}; use crate::persistence::ModelEvent; use crate::server::ids::{ClientId, SyncId}; use crate::workflows::workflow::Workflow; use crate::workflows::workflow_enum::{CloudWorkflowEnum, CloudWorkflowEnumModel, WorkflowEnum}; use crate::workflows::{CloudWorkflow, CloudWorkflowModel}; const LOCAL_OWNER_ID: &str = "local-galaxy-user"; /// Transitional local representation of a rule. /// /// The SQLite schema still stores rules in the legacy generic-string-object /// rows, so this alias preserves restart compatibility while callers move to /// the local repository API. pub type LocalRule = CloudAIFact; /// Transitional local representation of an execution profile. /// /// Execution profiles already live in the generic-string-object SQLite table. /// Keeping that representation lets Galaxy adopt existing profiles without a /// destructive database migration while removing their network sync path. pub type LocalExecutionProfile = CloudAIExecutionProfile; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LocalObjectRepositoryEvent { Rules, ExecutionProfiles { id: Option }, Notebooks { id: Option }, Workflows { id: Option }, } /// Galaxy-owned access to locally persisted objects. /// /// During the storage migration this repository writes the existing SQLite /// generic object rows and mirrors changes into `CloudModel` for consumers that /// have not moved yet. It never enqueues network work or requires an account. pub struct LocalObjectRepository { model_event_sender: Option>, legacy_profile_owner: Option, } impl LocalObjectRepository { pub fn new( model_event_sender: Option>, legacy_profile_owner: Option, ctx: &mut ModelContext, ) -> Self { let cloud_model = CloudModel::handle(ctx); ctx.subscribe_to_model(&cloud_model, |_, _, event, ctx| { if event_affects_rules(event) { ctx.emit(LocalObjectRepositoryEvent::Rules); ctx.notify(); } if let Some(id) = execution_profile_change(event) { ctx.emit(LocalObjectRepositoryEvent::ExecutionProfiles { id }); ctx.notify(); } if let Some(id) = notebook_change(event) { ctx.emit(LocalObjectRepositoryEvent::Notebooks { id }); ctx.notify(); } if let Some(id) = workflow_change(event) { ctx.emit(LocalObjectRepositoryEvent::Workflows { id }); ctx.notify(); } }); Self { model_event_sender, legacy_profile_owner, } } pub fn rules(&self, app: &AppContext) -> Vec { let cloud_model = CloudModel::as_ref(app); cloud_model .get_all_objects_of_type::() .filter(|rule| !rule.is_trashed(cloud_model)) .cloned() .collect() } pub fn rule(&self, id: &SyncId, app: &AppContext) -> Option { CloudModel::as_ref(app) .get_object_of_type::(id) .cloned() } pub fn create_rule(&mut self, fact: AIFact, ctx: &mut ModelContext) -> SyncId { let id = SyncId::ClientId(ClientId::new()); self.create_rule_with_id(id, fact, ctx); id } pub fn create_rule_with_id(&mut self, id: SyncId, fact: AIFact, ctx: &mut ModelContext) { let rule = new_local_rule(id, fact); self.upsert_rule(rule, ctx); } pub fn update_rule(&mut self, id: SyncId, fact: AIFact, ctx: &mut ModelContext) -> bool { let Some(mut rule) = self.rule(&id, ctx) else { return false; }; rule.set_model(CloudAIFactModel::new(fact)); set_locally_saved_metadata(&mut rule.metadata); self.upsert_rule(rule, ctx); true } pub fn delete_rule(&mut self, id: SyncId, ctx: &mut ModelContext) -> bool { if self.rule(&id, ctx).is_none() { return false; } CloudModel::handle(ctx).update(ctx, |cloud_model, ctx| { cloud_model.delete_object(id, ctx); }); self.save(ModelEvent::DeleteObjects { ids: vec![(id, ObjectIdType::GenericStringObject)], }); true } pub fn execution_profiles(&self, app: &AppContext) -> Vec { let cloud_model = CloudModel::as_ref(app); cloud_model .get_all_objects_of_type::() .filter(|profile| { !profile.is_trashed(cloud_model) && self.is_local_execution_profile(profile) }) .cloned() .collect() } pub fn execution_profile( &self, id: &SyncId, app: &AppContext, ) -> Option { let cloud_model = CloudModel::as_ref(app); cloud_model .get_object_of_type::(id) .filter(|profile| { !profile.is_trashed(cloud_model) && self.is_local_execution_profile(profile) }) .cloned() } pub fn create_execution_profile_with_id( &mut self, id: SyncId, profile: AIExecutionProfile, ctx: &mut ModelContext, ) { let profile = new_local_execution_profile(id, profile); self.upsert_execution_profile(profile, ctx); } pub fn update_execution_profile( &mut self, id: SyncId, profile: AIExecutionProfile, ctx: &mut ModelContext, ) -> bool { let Some(mut object) = self.execution_profile(&id, ctx) else { return false; }; object.set_model(CloudAIExecutionProfileModel::new(profile)); set_locally_saved_metadata(&mut object.metadata); self.upsert_execution_profile(object, ctx); true } pub fn delete_execution_profile(&mut self, id: SyncId, ctx: &mut ModelContext) -> bool { if self.execution_profile(&id, ctx).is_none() { return false; } CloudModel::handle(ctx).update(ctx, |cloud_model, ctx| { cloud_model.delete_object(id, ctx); }); self.save(ModelEvent::DeleteObjects { ids: vec![(id, ObjectIdType::GenericStringObject)], }); true } pub fn notebook(&self, id: &SyncId, app: &AppContext) -> Option { CloudModel::as_ref(app).get_notebook(id).cloned() } pub fn folder(&self, id: &SyncId, app: &AppContext) -> Option { CloudModel::as_ref(app).get_folder(id).cloned() } pub fn create_folder_with_id( &mut self, id: SyncId, name: String, parent_folder_id: Option, ctx: &mut ModelContext, ) { self.upsert_folder( new_local_folder(id, parent_folder_id, CloudFolderModel::new(&name, false)), ctx, ); } pub fn update_folder_name( &mut self, id: SyncId, name: String, ctx: &mut ModelContext, ) -> bool { let Some(mut folder) = self.folder(&id, ctx) else { return false; }; folder.set_model(CloudFolderModel { name, is_open: folder.model().is_open, is_warp_pack: folder.model().is_warp_pack, }); set_locally_saved_metadata(&mut folder.metadata); self.upsert_folder(folder, ctx); true } pub fn set_folder_trashed( &mut self, id: SyncId, trashed: bool, ctx: &mut ModelContext, ) -> bool { let Some(mut folder) = self.folder(&id, ctx) else { return false; }; folder.metadata.trashed_ts = trashed.then(|| ServerTimestamp::new(Utc::now())); set_locally_saved_metadata(&mut folder.metadata); self.upsert_folder(folder, ctx); true } pub fn delete_folder(&mut self, id: SyncId, ctx: &mut ModelContext) -> bool { if self.folder(&id, ctx).is_none() { return false; } let ids = CloudModel::handle(ctx).update(ctx, |cloud_model, ctx| { cloud_model.delete_object_and_descendants(id.uid(), ctx) }); if ids.is_empty() { return false; } self.save(ModelEvent::DeleteObjects { ids }); true } pub fn open_folder(&mut self, id: SyncId, ctx: &mut ModelContext) { self.set_folder_open_state(id, true, ctx); } pub fn close_folder(&mut self, id: SyncId, ctx: &mut ModelContext) { self.set_folder_open_state(id, false, ctx); } pub fn toggle_folder_open(&mut self, id: SyncId, ctx: &mut ModelContext) { let Some(folder) = self.folder(&id, ctx) else { return; }; self.set_folder_open_state(id, !folder.model().is_open, ctx); } pub fn collapse_local_folders_in_location( &mut self, location: CloudObjectLocation, ctx: &mut ModelContext, ) { let folder_ids = { let cloud_model = CloudModel::as_ref(ctx); cloud_model .get_all_active_and_inactive_folders() .filter(|folder| folder.permissions.owner == local_owner()) .filter(|folder| match location { CloudObjectLocation::Folder(parent_id) => { if folder.id == parent_id { return true; } let mut ancestor_id = folder.metadata.folder_id; while let Some(id) = ancestor_id { if id == parent_id { return true; } ancestor_id = cloud_model .get_folder(&id) .and_then(|ancestor| ancestor.metadata.folder_id); } false } CloudObjectLocation::Space(Space::Personal) => true, CloudObjectLocation::Space(Space::Shared) | CloudObjectLocation::Space(Space::Team { .. }) | CloudObjectLocation::Trash => false, }) .map(|folder| folder.id) .collect::>() }; for folder_id in folder_ids { self.close_folder(folder_id, ctx); } } fn set_folder_open_state(&self, id: SyncId, is_open: bool, ctx: &mut ModelContext) { let Some(mut folder) = self.folder(&id, ctx) else { return; }; folder.set_model(CloudFolderModel { name: folder.model().name.clone(), is_open, is_warp_pack: folder.model().is_warp_pack, }); self.upsert_folder(folder, ctx); } pub fn create_notebook_with_id( &mut self, id: SyncId, folder_id: Option, notebook: CloudNotebookModel, ctx: &mut ModelContext, ) { self.upsert_notebook(new_local_notebook(id, folder_id, notebook), ctx); } pub fn update_notebook( &mut self, id: SyncId, notebook: CloudNotebookModel, ctx: &mut ModelContext, ) -> bool { let Some(mut object) = self.notebook(&id, ctx) else { return false; }; object.set_model(notebook); set_locally_saved_metadata(&mut object.metadata); self.upsert_notebook(object, ctx); true } pub fn update_notebook_data( &mut self, id: SyncId, data: String, ctx: &mut ModelContext, ) -> bool { let Some(notebook) = self.notebook(&id, ctx) else { return false; }; self.update_notebook( id, CloudNotebookModel { title: notebook.model().title.clone(), data, ai_document_id: notebook.model().ai_document_id, conversation_id: notebook.model().conversation_id.clone(), }, ctx, ) } pub fn update_notebook_title( &mut self, id: SyncId, title: String, ctx: &mut ModelContext, ) -> bool { let Some(notebook) = self.notebook(&id, ctx) else { return false; }; self.update_notebook( id, CloudNotebookModel { title, data: notebook.model().data.clone(), ai_document_id: notebook.model().ai_document_id, conversation_id: notebook.model().conversation_id.clone(), }, ctx, ) } pub fn delete_notebook(&mut self, id: SyncId, ctx: &mut ModelContext) -> bool { if self.notebook(&id, ctx).is_none() { return false; } self.delete_local_object(id, ObjectIdType::Notebook, ctx) } pub fn duplicate_notebook( &mut self, id: SyncId, ctx: &mut ModelContext, ) -> Option { let source = self.notebook(&id, ctx)?; let cloud_model = CloudModel::as_ref(ctx); let mut model = source.model().clone(); model.title = next_duplicate_name(&model.title, |candidate| { cloud_model .get_all_active_notebooks() .any(|notebook| notebook.model().title == candidate) }); let duplicate_id = SyncId::ClientId(ClientId::new()); self.create_notebook_with_id(duplicate_id, source.metadata.folder_id, model, ctx); Some(duplicate_id) } pub fn set_notebook_trashed( &mut self, id: SyncId, trashed: bool, ctx: &mut ModelContext, ) -> bool { let Some(mut notebook) = self.notebook(&id, ctx) else { return false; }; notebook.metadata.trashed_ts = trashed.then(|| ServerTimestamp::new(Utc::now())); set_locally_saved_metadata(&mut notebook.metadata); self.upsert_notebook(notebook, ctx); true } pub fn workflow(&self, id: &SyncId, app: &AppContext) -> Option { CloudModel::as_ref(app).get_workflow(id).cloned() } pub fn env_var_collection( &self, id: &SyncId, app: &AppContext, ) -> Option { CloudModel::as_ref(app) .get_object_of_type::(id) .cloned() } pub fn create_env_var_collection_with_id( &mut self, id: SyncId, folder_id: Option, collection: EnvVarCollection, ctx: &mut ModelContext, ) { self.upsert_generic_string_object( GenericCloudObject::new( id, CloudEnvVarCollectionModel::new(collection), locally_saved_metadata(folder_id), local_permissions(), ), ctx, ); } pub fn update_env_var_collection( &mut self, id: SyncId, collection: EnvVarCollection, ctx: &mut ModelContext, ) -> bool { let Some(mut object) = self.env_var_collection(&id, ctx) else { return false; }; object.set_model(CloudEnvVarCollectionModel::new(collection)); set_locally_saved_metadata(&mut object.metadata); self.upsert_generic_string_object(object, ctx); true } pub fn delete_env_var_collection(&mut self, id: SyncId, ctx: &mut ModelContext) -> bool { if self.env_var_collection(&id, ctx).is_none() { return false; } self.delete_local_object(id, ObjectIdType::GenericStringObject, ctx) } pub fn set_env_var_collection_trashed( &mut self, id: SyncId, trashed: bool, ctx: &mut ModelContext, ) -> bool { let Some(mut object) = self.env_var_collection(&id, ctx) else { return false; }; object.metadata.trashed_ts = trashed.then(|| ServerTimestamp::new(Utc::now())); set_locally_saved_metadata(&mut object.metadata); self.upsert_generic_string_object(object, ctx); true } pub fn duplicate_env_var_collection( &mut self, id: SyncId, ctx: &mut ModelContext, ) -> Option { let source = self.env_var_collection(&id, ctx)?; let cloud_model = CloudModel::as_ref(ctx); let mut collection = source.model().string_model.clone(); let title = collection.title.clone().unwrap_or_default(); let name = next_duplicate_name(title.as_str(), |candidate| { cloud_model .get_all_active_env_var_collections() .any(|collection| { collection.model().string_model.title.as_deref() == Some(candidate) }) }); collection.title = Some(name); let duplicate_id = SyncId::ClientId(ClientId::new()); self.create_env_var_collection_with_id( duplicate_id, source.metadata.folder_id, collection, ctx, ); Some(duplicate_id) } pub fn templatable_mcp_server( &self, uuid: uuid::Uuid, app: &AppContext, ) -> Option { CloudModel::as_ref(app) .get_all_objects_of_type::() .find(|server| server.model().string_model.uuid == uuid) .cloned() } pub fn create_templatable_mcp_server_with_id( &mut self, id: SyncId, server: TemplatableMCPServer, ctx: &mut ModelContext, ) { self.upsert_templatable_mcp_server( GenericCloudObject::new( id, CloudTemplatableMCPServerModel::new(server), locally_saved_metadata(None), local_permissions(), ), ctx, ); } pub fn update_templatable_mcp_server( &mut self, uuid: uuid::Uuid, server: TemplatableMCPServer, ctx: &mut ModelContext, ) -> bool { let Some(mut object) = self.templatable_mcp_server(uuid, ctx) else { return false; }; object.set_model(CloudTemplatableMCPServerModel::new(server)); set_locally_saved_metadata(&mut object.metadata); self.upsert_templatable_mcp_server(object, ctx); true } pub fn delete_templatable_mcp_server( &mut self, uuid: uuid::Uuid, ctx: &mut ModelContext, ) -> bool { let Some(object) = self.templatable_mcp_server(uuid, ctx) else { return false; }; self.delete_local_object(object.id, ObjectIdType::GenericStringObject, ctx) } pub fn create_workflow_with_id( &mut self, id: SyncId, folder_id: Option, workflow: Workflow, ctx: &mut ModelContext, ) { self.upsert_workflow(new_local_workflow(id, folder_id, workflow), ctx); } pub fn create_workflow_enum_with_id( &mut self, id: SyncId, workflow_enum: WorkflowEnum, ctx: &mut ModelContext, ) { self.upsert_workflow_enum( GenericCloudObject::new( id, CloudWorkflowEnumModel::new(workflow_enum), locally_saved_metadata(None), local_permissions(), ), ctx, ); } pub fn update_workflow( &mut self, id: SyncId, workflow: Workflow, ctx: &mut ModelContext, ) -> bool { let Some(mut object) = self.workflow(&id, ctx) else { return false; }; object.set_model(CloudWorkflowModel::new(workflow)); set_locally_saved_metadata(&mut object.metadata); self.upsert_workflow(object, ctx); true } pub fn delete_workflow(&mut self, id: SyncId, ctx: &mut ModelContext) -> bool { if self.workflow(&id, ctx).is_none() { return false; } self.delete_local_object(id, ObjectIdType::Workflow, ctx) } pub fn duplicate_workflow( &mut self, id: SyncId, ctx: &mut ModelContext, ) -> Option { let source = self.workflow(&id, ctx)?; let cloud_model = CloudModel::as_ref(ctx); let mut workflow = source.model().data.clone(); let name = next_duplicate_name(workflow.name(), |candidate| { cloud_model .get_all_active_workflows() .any(|workflow| workflow.model().data.name() == candidate) }); workflow.set_name(&name); let duplicate_id = SyncId::ClientId(ClientId::new()); self.create_workflow_with_id(duplicate_id, source.metadata.folder_id, workflow, ctx); Some(duplicate_id) } pub fn set_workflow_trashed( &mut self, id: SyncId, trashed: bool, ctx: &mut ModelContext, ) -> bool { let Some(mut workflow) = self.workflow(&id, ctx) else { return false; }; workflow.metadata.trashed_ts = trashed.then(|| ServerTimestamp::new(Utc::now())); set_locally_saved_metadata(&mut workflow.metadata); self.upsert_workflow(workflow, ctx); true } fn upsert_rule(&self, rule: LocalRule, ctx: &mut ModelContext) { CloudModel::handle(ctx).update(ctx, |cloud_model, ctx| { cloud_model.upsert_local_object(rule.clone(), ctx); }); self.save(ModelEvent::UpsertGenericStringObject { object: Box::new(rule), }); } fn upsert_execution_profile( &self, profile: LocalExecutionProfile, ctx: &mut ModelContext, ) { CloudModel::handle(ctx).update(ctx, |cloud_model, ctx| { cloud_model.upsert_local_object(profile.clone(), ctx); }); self.save(ModelEvent::UpsertGenericStringObject { object: Box::new(profile), }); } fn upsert_generic_string_object( &self, object: GenericCloudObject, ctx: &mut ModelContext, ) { CloudModel::handle(ctx).update(ctx, |cloud_model, ctx| { cloud_model.upsert_local_object(object.clone(), ctx); }); self.save(ModelEvent::UpsertGenericStringObject { object: Box::new(object), }); } fn upsert_templatable_mcp_server( &self, object: CloudTemplatableMCPServer, ctx: &mut ModelContext, ) { CloudModel::handle(ctx).update(ctx, |cloud_model, ctx| { cloud_model.upsert_local_object(object.clone(), ctx); }); self.save(ModelEvent::UpsertGenericStringObject { object: Box::new(object), }); } fn upsert_notebook(&self, notebook: CloudNotebook, ctx: &mut ModelContext) { CloudModel::handle(ctx).update(ctx, |cloud_model, ctx| { cloud_model.upsert_local_object(notebook.clone(), ctx); }); self.save(ModelEvent::UpsertNotebook { notebook }); } fn upsert_folder(&self, folder: CloudFolder, ctx: &mut ModelContext) { CloudModel::handle(ctx).update(ctx, |cloud_model, ctx| { cloud_model.upsert_local_object(folder.clone(), ctx); }); self.save(ModelEvent::UpsertFolder { folder }); } fn upsert_workflow(&self, workflow: CloudWorkflow, ctx: &mut ModelContext) { CloudModel::handle(ctx).update(ctx, |cloud_model, ctx| { cloud_model.upsert_local_object(workflow.clone(), ctx); }); self.save(ModelEvent::UpsertWorkflow { workflow }); } fn upsert_workflow_enum(&self, workflow_enum: CloudWorkflowEnum, ctx: &mut ModelContext) { CloudModel::handle(ctx).update(ctx, |cloud_model, ctx| { cloud_model.upsert_local_object(workflow_enum.clone(), ctx); }); self.save(ModelEvent::UpsertGenericStringObject { object: Box::new(workflow_enum), }); } fn delete_local_object( &self, id: SyncId, object_type: ObjectIdType, ctx: &mut ModelContext, ) -> bool { CloudModel::handle(ctx).update(ctx, |cloud_model, ctx| { cloud_model.delete_object(id, ctx); }); self.save(ModelEvent::DeleteObjects { ids: vec![(id, object_type)], }); true } fn is_local_execution_profile(&self, profile: &LocalExecutionProfile) -> bool { profile.permissions().owner == local_owner() || self .legacy_profile_owner .is_some_and(|owner| profile.permissions().owner == owner) } fn save(&self, event: ModelEvent) { let Some(sender) = &self.model_event_sender else { return; }; if let Err(error) = sender.send(event) { log::error!("Failed to save local object: {error}"); } } } impl Entity for LocalObjectRepository { type Event = LocalObjectRepositoryEvent; } impl SingletonEntity for LocalObjectRepository {} pub(crate) fn new_local_rule(id: SyncId, fact: AIFact) -> LocalRule { GenericCloudObject::new( id, CloudAIFactModel::new(fact), locally_saved_metadata(None), local_permissions(), ) } pub(crate) fn new_local_notebook( id: SyncId, folder_id: Option, notebook: CloudNotebookModel, ) -> CloudNotebook { CloudNotebook::new( id, notebook, locally_saved_metadata(folder_id), local_permissions(), ) } pub(crate) fn new_local_folder( id: SyncId, folder_id: Option, folder: CloudFolderModel, ) -> CloudFolder { CloudFolder::new( id, folder, locally_saved_metadata(folder_id), local_permissions(), ) } pub(crate) fn new_local_workflow( id: SyncId, folder_id: Option, workflow: Workflow, ) -> CloudWorkflow { CloudWorkflow::new( id, CloudWorkflowModel::new(workflow), locally_saved_metadata(folder_id), local_permissions(), ) } fn new_local_execution_profile(id: SyncId, profile: AIExecutionProfile) -> LocalExecutionProfile { GenericCloudObject::new( id, CloudAIExecutionProfileModel::new(profile), locally_saved_metadata(None), local_permissions(), ) } pub(crate) fn local_owner() -> Owner { Owner::User { user_uid: UserUid::new(LOCAL_OWNER_ID), } } fn local_permissions() -> CloudObjectPermissions { CloudObjectPermissions { owner: local_owner(), permissions_last_updated_ts: None, anyone_with_link: None, guests: Vec::new(), } } fn locally_saved_metadata(folder_id: Option) -> CloudObjectMetadata { let now = ServerTimestamp::new(Utc::now()); CloudObjectMetadata { revision: Some(Revision::from(now)), metadata_last_updated_ts: Some(now), current_editor_uid: None, pending_changes_statuses: locally_saved_statuses(), trashed_ts: None, folder_id, is_welcome_object: false, last_editor_uid: None, creator_uid: None, last_task_run_ts: None, } } fn set_locally_saved_metadata(metadata: &mut CloudObjectMetadata) { let now = ServerTimestamp::new(Utc::now()); metadata.revision = Some(Revision::from(now)); metadata.metadata_last_updated_ts = Some(now); metadata.pending_changes_statuses = locally_saved_statuses(); } fn locally_saved_statuses() -> CloudObjectStatuses { CloudObjectStatuses { content_sync_status: CloudObjectSyncStatus::NoLocalChanges, has_pending_metadata_change: false, has_pending_permissions_change: false, pending_untrash: false, pending_delete: false, } } fn next_duplicate_name(original_name: &str, mut exists: impl FnMut(&str) -> bool) -> String { let mut copy_number = 1; loop { let candidate = format!("{original_name} ({copy_number})"); if !exists(&candidate) { return candidate; } copy_number += 1; } } fn event_affects_rules(event: &CloudModelEvent) -> bool { match event { CloudModelEvent::ObjectMoved { type_and_id, .. } | CloudModelEvent::ObjectUpdated { type_and_id, .. } | CloudModelEvent::ObjectTrashed { type_and_id, .. } | CloudModelEvent::ObjectUntrashed { type_and_id, .. } | CloudModelEvent::ObjectCreated { type_and_id } | CloudModelEvent::ObjectDeleted { type_and_id, .. } | CloudModelEvent::ObjectPermissionsUpdated { type_and_id, .. } | CloudModelEvent::ObjectSynced { type_and_id, .. } => is_rule_type(type_and_id), CloudModelEvent::InitialLoadCompleted => true, CloudModelEvent::NotebookEditorChangedFromServer { .. } | CloudModelEvent::ObjectForceExpanded { .. } => false, } } fn execution_profile_change(event: &CloudModelEvent) -> Option> { match event { CloudModelEvent::ObjectMoved { type_and_id, .. } | CloudModelEvent::ObjectUpdated { type_and_id, .. } | CloudModelEvent::ObjectTrashed { type_and_id, .. } | CloudModelEvent::ObjectUntrashed { type_and_id, .. } | CloudModelEvent::ObjectCreated { type_and_id } | CloudModelEvent::ObjectDeleted { type_and_id, .. } | CloudModelEvent::ObjectPermissionsUpdated { type_and_id, .. } | CloudModelEvent::ObjectSynced { type_and_id, .. } => match type_and_id { CloudObjectTypeAndId::GenericStringObject { object_type: GenericStringObjectFormat::Json(JsonObjectType::AIExecutionProfile), id, } => Some(Some(*id)), CloudObjectTypeAndId::Notebook(_) | CloudObjectTypeAndId::Workflow(_) | CloudObjectTypeAndId::Folder(_) | CloudObjectTypeAndId::GenericStringObject { .. } => None, }, CloudModelEvent::InitialLoadCompleted => Some(None), CloudModelEvent::NotebookEditorChangedFromServer { .. } | CloudModelEvent::ObjectForceExpanded { .. } => None, } } fn notebook_change(event: &CloudModelEvent) -> Option> { match event { CloudModelEvent::ObjectMoved { type_and_id, .. } | CloudModelEvent::ObjectUpdated { type_and_id, .. } | CloudModelEvent::ObjectTrashed { type_and_id, .. } | CloudModelEvent::ObjectUntrashed { type_and_id, .. } | CloudModelEvent::ObjectCreated { type_and_id } | CloudModelEvent::ObjectDeleted { type_and_id, .. } | CloudModelEvent::ObjectPermissionsUpdated { type_and_id, .. } | CloudModelEvent::ObjectSynced { type_and_id, .. } => match type_and_id { CloudObjectTypeAndId::Notebook(id) => Some(Some(*id)), CloudObjectTypeAndId::Workflow(_) | CloudObjectTypeAndId::Folder(_) | CloudObjectTypeAndId::GenericStringObject { .. } => None, }, CloudModelEvent::InitialLoadCompleted => Some(None), CloudModelEvent::NotebookEditorChangedFromServer { .. } | CloudModelEvent::ObjectForceExpanded { .. } => None, } } fn workflow_change(event: &CloudModelEvent) -> Option> { match event { CloudModelEvent::ObjectMoved { type_and_id, .. } | CloudModelEvent::ObjectUpdated { type_and_id, .. } | CloudModelEvent::ObjectTrashed { type_and_id, .. } | CloudModelEvent::ObjectUntrashed { type_and_id, .. } | CloudModelEvent::ObjectCreated { type_and_id } | CloudModelEvent::ObjectDeleted { type_and_id, .. } | CloudModelEvent::ObjectPermissionsUpdated { type_and_id, .. } | CloudModelEvent::ObjectSynced { type_and_id, .. } => match type_and_id { CloudObjectTypeAndId::Workflow(id) => Some(Some(*id)), CloudObjectTypeAndId::Notebook(_) | CloudObjectTypeAndId::Folder(_) | CloudObjectTypeAndId::GenericStringObject { .. } => None, }, CloudModelEvent::InitialLoadCompleted => Some(None), CloudModelEvent::NotebookEditorChangedFromServer { .. } | CloudModelEvent::ObjectForceExpanded { .. } => None, } } fn is_rule_type(type_and_id: &CloudObjectTypeAndId) -> bool { matches!( type_and_id, CloudObjectTypeAndId::GenericStringObject { object_type: GenericStringObjectFormat::Json(JsonObjectType::AIFact), .. } ) } #[cfg(test)] #[path = "local_object_repository_tests.rs"] mod tests;