Files
galaxy/app/src/local_object_repository.rs
T

1020 lines
34 KiB
Rust

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<SyncId> },
Notebooks { id: Option<SyncId> },
Workflows { id: Option<SyncId> },
}
/// 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<SyncSender<ModelEvent>>,
legacy_profile_owner: Option<Owner>,
}
impl LocalObjectRepository {
pub fn new(
model_event_sender: Option<SyncSender<ModelEvent>>,
legacy_profile_owner: Option<Owner>,
ctx: &mut ModelContext<Self>,
) -> 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<LocalRule> {
let cloud_model = CloudModel::as_ref(app);
cloud_model
.get_all_objects_of_type::<GenericStringObjectId, CloudAIFactModel>()
.filter(|rule| !rule.is_trashed(cloud_model))
.cloned()
.collect()
}
pub fn rule(&self, id: &SyncId, app: &AppContext) -> Option<LocalRule> {
CloudModel::as_ref(app)
.get_object_of_type::<GenericStringObjectId, CloudAIFactModel>(id)
.cloned()
}
pub fn create_rule(&mut self, fact: AIFact, ctx: &mut ModelContext<Self>) -> 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<Self>) {
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<Self>) -> 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<Self>) -> 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<LocalExecutionProfile> {
let cloud_model = CloudModel::as_ref(app);
cloud_model
.get_all_objects_of_type::<GenericStringObjectId, CloudAIExecutionProfileModel>()
.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<LocalExecutionProfile> {
let cloud_model = CloudModel::as_ref(app);
cloud_model
.get_object_of_type::<GenericStringObjectId, CloudAIExecutionProfileModel>(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<Self>,
) {
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<Self>,
) -> 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<Self>) -> 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<CloudNotebook> {
CloudModel::as_ref(app).get_notebook(id).cloned()
}
pub fn folder(&self, id: &SyncId, app: &AppContext) -> Option<CloudFolder> {
CloudModel::as_ref(app).get_folder(id).cloned()
}
pub fn create_folder_with_id(
&mut self,
id: SyncId,
name: String,
parent_folder_id: Option<SyncId>,
ctx: &mut ModelContext<Self>,
) {
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<Self>,
) -> 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<Self>,
) -> 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<Self>) -> 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>) {
self.set_folder_open_state(id, true, ctx);
}
pub fn close_folder(&mut self, id: SyncId, ctx: &mut ModelContext<Self>) {
self.set_folder_open_state(id, false, ctx);
}
pub fn toggle_folder_open(&mut self, id: SyncId, ctx: &mut ModelContext<Self>) {
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<Self>,
) {
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::<Vec<_>>()
};
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<Self>) {
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<SyncId>,
notebook: CloudNotebookModel,
ctx: &mut ModelContext<Self>,
) {
self.upsert_notebook(new_local_notebook(id, folder_id, notebook), ctx);
}
pub fn update_notebook(
&mut self,
id: SyncId,
notebook: CloudNotebookModel,
ctx: &mut ModelContext<Self>,
) -> 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<Self>,
) -> 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<Self>,
) -> 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<Self>) -> 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<Self>,
) -> Option<SyncId> {
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<Self>,
) -> 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<CloudWorkflow> {
CloudModel::as_ref(app).get_workflow(id).cloned()
}
pub fn env_var_collection(
&self,
id: &SyncId,
app: &AppContext,
) -> Option<CloudEnvVarCollection> {
CloudModel::as_ref(app)
.get_object_of_type::<GenericStringObjectId, CloudEnvVarCollectionModel>(id)
.cloned()
}
pub fn create_env_var_collection_with_id(
&mut self,
id: SyncId,
folder_id: Option<SyncId>,
collection: EnvVarCollection,
ctx: &mut ModelContext<Self>,
) {
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<Self>,
) -> 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<Self>) -> 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<Self>,
) -> 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<Self>,
) -> Option<SyncId> {
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<CloudTemplatableMCPServer> {
CloudModel::as_ref(app)
.get_all_objects_of_type::<GenericStringObjectId, CloudTemplatableMCPServerModel>()
.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>,
) {
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<Self>,
) -> 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<Self>,
) -> 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<SyncId>,
workflow: Workflow,
ctx: &mut ModelContext<Self>,
) {
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>,
) {
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<Self>,
) -> 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<Self>) -> 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<Self>,
) -> Option<SyncId> {
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<Self>,
) -> 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<Self>) {
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<Self>,
) {
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<GenericStringObjectId, CloudEnvVarCollectionModel>,
ctx: &mut ModelContext<Self>,
) {
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<Self>,
) {
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<Self>) {
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<Self>) {
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<Self>) {
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<Self>) {
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<Self>,
) -> 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<SyncId>,
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<SyncId>,
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<SyncId>,
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<SyncId>) -> 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<Option<SyncId>> {
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<Option<SyncId>> {
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<Option<SyncId>> {
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;