1656 lines
69 KiB
Rust
1656 lines
69 KiB
Rust
use crate::{
|
|
ai::{
|
|
ambient_agents::scheduled::ScheduledAmbientAgent,
|
|
cloud_environments::AmbientAgentEnvironment,
|
|
document::ai_document_model::AIDocumentId,
|
|
execution_profiles::AIExecutionProfile,
|
|
facts::AIFact,
|
|
mcp::{MCPServer, TemplatableMCPServer},
|
|
},
|
|
channel::ChannelState,
|
|
cloud_object::{
|
|
model::{
|
|
actions::{ObjectActionHistory, ObjectActionType},
|
|
generic_string_model::{
|
|
GenericStringModel, GenericStringObjectId, Serializer, StringModel,
|
|
},
|
|
json_model::JsonSerializer,
|
|
},
|
|
BulkCreateCloudObjectResult, BulkCreateGenericStringObjectsRequest,
|
|
CreateCloudObjectResult, CreateObjectRequest, CreatedCloudObject, GenericCloudObject,
|
|
GenericServerObject, GenericStringObjectFormat, GenericStringObjectUniqueKey,
|
|
JsonObjectType, ObjectDeleteResult, ObjectIdType, ObjectMetadataUpdateResult,
|
|
ObjectPermissionUpdateResult, ObjectPermissionsUpdateData, ObjectType, ObjectsToUpdate,
|
|
Owner, Revision, RevisionAndLastEditor, ServerCloudObject, ServerFolder, ServerMetadata,
|
|
ServerNotebook, ServerObject, ServerPermissions, ServerWorkflow, UpdateCloudObjectResult,
|
|
},
|
|
drive::{folders::FolderId, sharing::SharingAccessLevel},
|
|
env_vars::EnvVarCollection,
|
|
notebooks::{NotebookId, SerializedNotebook},
|
|
server::{
|
|
cloud_objects::{
|
|
listener::ObjectUpdateMessage,
|
|
update_manager::{GetCloudObjectResponse, InitialLoadResponse},
|
|
},
|
|
graphql::{get_request_context, get_user_facing_error_message},
|
|
ids::{ClientId, HashableId, ServerId, ServerIdAndType, SyncId, ToServerId},
|
|
server_api::{auth::AuthClient, ServerApi},
|
|
sync_queue::SerializedModel,
|
|
},
|
|
settings::Preference,
|
|
workflows::{workflow_enum::WorkflowEnum, WorkflowId},
|
|
workspaces::user_profiles::UserProfileWithUID,
|
|
};
|
|
use anyhow::{anyhow, Context, Result};
|
|
use async_channel::Sender;
|
|
use async_trait::async_trait;
|
|
use chrono::{DateTime, Utc};
|
|
use cynic::{MutationBuilder, QueryBuilder, SubscriptionBuilder};
|
|
#[cfg(test)]
|
|
use mockall::{automock, predicate::*};
|
|
use std::collections::HashMap;
|
|
use warp_core::report_error;
|
|
use warp_graphql::{
|
|
error::UserFacingErrorInterface,
|
|
generic_string_object::GenericStringObjectInput,
|
|
mutations::{
|
|
add_object_guests::{
|
|
AddObjectGuests, AddObjectGuestsInput, AddObjectGuestsResult, AddObjectGuestsVariables,
|
|
},
|
|
bulk_create_objects::{
|
|
BulkCreateGenericStringObjectsInput, BulkCreateObjects, BulkCreateObjectsInput,
|
|
BulkCreateObjectsResult, BulkCreateObjectsVariables,
|
|
},
|
|
create_folder::{
|
|
CreateFolder, CreateFolderInput, CreateFolderResult, CreateFolderVariables,
|
|
},
|
|
create_generic_string_object::{
|
|
CreateGenericStringObject, CreateGenericStringObjectInput,
|
|
CreateGenericStringObjectResult, CreateGenericStringObjectVariables,
|
|
},
|
|
create_notebook::{
|
|
CreateNotebook, CreateNotebookInput, CreateNotebookResult, CreateNotebookVariables,
|
|
},
|
|
create_workflow::{
|
|
CreateWorkflow, CreateWorkflowInput, CreateWorkflowResult, CreateWorkflowVariables,
|
|
},
|
|
delete_object::{
|
|
DeleteObject, DeleteObjectInput, DeleteObjectResult, DeleteObjectVariables,
|
|
},
|
|
empty_trash::{EmptyTrash, EmptyTrashInput, EmptyTrashResult, EmptyTrashVariables},
|
|
give_up_notebook_edit_access::{
|
|
GiveUpNotebookEditAccess, GiveUpNotebookEditAccessVariables,
|
|
},
|
|
grab_notebook_edit_access::{GrabNotebookEditAccess, GrabNotebookEditAccessVariables},
|
|
leave_object::{LeaveObject, LeaveObjectInput, LeaveObjectResult, LeaveObjectVariables},
|
|
move_object::{MoveObject, MoveObjectInput, MoveObjectResult, MoveObjectVariables},
|
|
record_object_action::{
|
|
RecordObjectAction, RecordObjectActionInput, RecordObjectActionResult,
|
|
RecordObjectActionVariables,
|
|
},
|
|
remove_object_guest::{
|
|
RemoveObjectGuest, RemoveObjectGuestInput, RemoveObjectGuestResult,
|
|
RemoveObjectGuestVariables,
|
|
},
|
|
remove_object_link_permissions::{
|
|
RemoveObjectLinkPermissions, RemoveObjectLinkPermissionsInput,
|
|
RemoveObjectLinkPermissionsResult, RemoveObjectLinkPermissionsVariables,
|
|
},
|
|
set_object_link_permissions::{
|
|
SetObjectLinkPermissions, SetObjectLinkPermissionsInput,
|
|
SetObjectLinkPermissionsResult, SetObjectLinkPermissionsVariables,
|
|
},
|
|
transfer_generic_string_object_owner::{
|
|
TransferGenericStringObjectOwner, TransferGenericStringObjectOwnerInput,
|
|
TransferGenericStringObjectOwnerResult, TransferGenericStringObjectOwnerVariables,
|
|
},
|
|
transfer_notebook_owner::{
|
|
TransferNotebookOwner, TransferNotebookOwnerInput, TransferNotebookOwnerResult,
|
|
TransferNotebookOwnerVariables,
|
|
},
|
|
transfer_workflow_owner::{
|
|
TransferWorkflowOwner, TransferWorkflowOwnerInput, TransferWorkflowOwnerResult,
|
|
TransferWorkflowOwnerVariables,
|
|
},
|
|
trash_object::{TrashObject, TrashObjectInput, TrashObjectResult, TrashObjectVariables},
|
|
untrash_object::{UntrashObject, UntrashObjectInput, UntrashObjectVariables},
|
|
update_folder::{
|
|
UpdateFolder, UpdateFolderInput, UpdateFolderResult, UpdateFolderVariables,
|
|
},
|
|
update_generic_string_object::{
|
|
UpdateGenericStringObject, UpdateGenericStringObjectInput,
|
|
UpdateGenericStringObjectVariables,
|
|
},
|
|
update_notebook::{
|
|
NotebookUpdate, UpdateNotebook, UpdateNotebookInput, UpdateNotebookResult,
|
|
UpdateNotebookVariables,
|
|
},
|
|
update_object_guests::{
|
|
UpdateObjectGuests, UpdateObjectGuestsInput, UpdateObjectGuestsResult,
|
|
UpdateObjectGuestsVariables,
|
|
},
|
|
update_workflow::{
|
|
UpdateWorkflow, UpdateWorkflowInput, UpdateWorkflowResult, UpdateWorkflowVariables,
|
|
WorkflowUpdate,
|
|
},
|
|
},
|
|
notebook::{UpdateNotebookEditAccessInput, UpdateNotebookEditAccessResult},
|
|
object::CloudObjectWithDescendants,
|
|
object_permissions::AccessLevel,
|
|
queries::{
|
|
get_cloud_environments::{
|
|
GetCloudEnvironmentsQuery, GetCloudEnvironmentsQueryVariables,
|
|
GetCloudEnvironmentsResult,
|
|
},
|
|
get_cloud_object::{
|
|
CloudObjectInput, CloudObjectResult, GetCloudObject, GetCloudObjectVariables,
|
|
},
|
|
get_updated_cloud_objects::{
|
|
GetUpdatedCloudObjects, GetUpdatedCloudObjectsVariables, UpdatedCloudObjectsInput,
|
|
UpdatedCloudObjectsResult,
|
|
},
|
|
},
|
|
subscriptions::{
|
|
get_warp_drive_updates::GetWarpDriveUpdates, start_graphql_streaming_operation,
|
|
},
|
|
};
|
|
|
|
/// Identifies a guest to remove from an object.
|
|
#[derive(Clone, Debug)]
|
|
pub enum GuestIdentifier {
|
|
/// Remove a user guest by their email address.
|
|
Email(String),
|
|
/// Remove a team guest by their team UID.
|
|
TeamUid(ServerId),
|
|
}
|
|
|
|
#[cfg_attr(test, automock)]
|
|
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
|
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
|
pub trait ObjectClient: 'static + Send + Sync {
|
|
/// This method saves a workflow for a given owner and returns it on success.
|
|
async fn create_workflow(
|
|
&self,
|
|
request: CreateObjectRequest,
|
|
) -> Result<CreateCloudObjectResult>;
|
|
|
|
/// Updates a workflow with the new data. The update may be rejected if a revision
|
|
/// is specified _and_ that revision is not the current revision of the object in storage.
|
|
async fn update_workflow(
|
|
&self,
|
|
workflow_id: WorkflowId,
|
|
data: SerializedModel,
|
|
revision: Option<Revision>,
|
|
) -> Result<UpdateCloudObjectResult<ServerWorkflow>>;
|
|
|
|
/// Creates n generic string objects in a single graphql request. Use
|
|
/// this rather than calling create_generic_string_object multiple times
|
|
/// in a loop.
|
|
async fn bulk_create_generic_string_objects(
|
|
&self,
|
|
owner: Owner,
|
|
objects: &[BulkCreateGenericStringObjectsRequest],
|
|
) -> Result<BulkCreateCloudObjectResult>;
|
|
|
|
async fn create_generic_string_object(
|
|
&self,
|
|
format: GenericStringObjectFormat,
|
|
uniqueness_key: Option<GenericStringObjectUniqueKey>,
|
|
request: CreateObjectRequest,
|
|
) -> Result<CreateCloudObjectResult>;
|
|
|
|
/// Creates a notebook on the server, returning the ID and revision of the object after
|
|
/// creation.
|
|
async fn create_notebook(
|
|
&self,
|
|
request: CreateObjectRequest,
|
|
) -> Result<CreateCloudObjectResult>;
|
|
|
|
/// Updates a notebook with the new title and data. The update may be rejected if a revision
|
|
/// is specified _and_ that revision is not the current revision of the object in storage.
|
|
async fn update_notebook(
|
|
&self,
|
|
notebook_id: NotebookId,
|
|
title: Option<String>,
|
|
data: Option<SerializedModel>,
|
|
revision: Option<Revision>,
|
|
) -> Result<UpdateCloudObjectResult<ServerNotebook>>;
|
|
|
|
async fn create_folder(&self, request: CreateObjectRequest) -> Result<CreateCloudObjectResult>;
|
|
|
|
async fn update_folder(
|
|
&self,
|
|
folder_id: FolderId,
|
|
name: SerializedModel,
|
|
) -> Result<UpdateCloudObjectResult<ServerFolder>>;
|
|
|
|
async fn update_generic_string_object(
|
|
&self,
|
|
object_id: GenericStringObjectId,
|
|
model: SerializedModel,
|
|
revision: Option<Revision>,
|
|
) -> Result<UpdateCloudObjectResult<Box<dyn ServerObject>>>;
|
|
|
|
/// Sets the current editor of the notebook to be the logged in user
|
|
async fn grab_notebook_edit_access(&self, notebook_id: NotebookId) -> Result<ServerMetadata>;
|
|
/// Sets the current editor of the notebook to be null
|
|
async fn give_up_notebook_edit_access(&self, notebook_id: NotebookId)
|
|
-> Result<ServerMetadata>;
|
|
|
|
/// Gets updates for all Warp Drive actions.
|
|
async fn get_warp_drive_updates(
|
|
&self,
|
|
message_sender: Sender<ObjectUpdateMessage>,
|
|
stream_ready_sender: Sender<()>,
|
|
) -> Result<()>;
|
|
|
|
async fn fetch_changed_objects(
|
|
&self,
|
|
objects_to_update: ObjectsToUpdate,
|
|
force_refresh: bool,
|
|
) -> Result<InitialLoadResponse>;
|
|
|
|
async fn fetch_single_cloud_object(&self, id: ServerId) -> Result<GetCloudObjectResponse>;
|
|
|
|
// Transfers a notebook to the given owner
|
|
async fn transfer_notebook_owner(&self, notebook_id: NotebookId, owner: Owner) -> Result<bool>;
|
|
|
|
async fn transfer_workflow_owner(&self, workflow_id: WorkflowId, owner: Owner) -> Result<bool>;
|
|
|
|
async fn transfer_generic_string_object_owner(
|
|
&self,
|
|
workflow_id: GenericStringObjectId,
|
|
owner: Owner,
|
|
) -> Result<bool>;
|
|
|
|
async fn trash_object(&self, id: ServerId) -> Result<bool>;
|
|
|
|
async fn untrash_object(&self, id: ServerId) -> Result<ObjectMetadataUpdateResult>;
|
|
|
|
async fn delete_object(&self, id: ServerId) -> Result<ObjectDeleteResult>;
|
|
|
|
async fn empty_trash(&self, owner: Owner) -> Result<ObjectDeleteResult>;
|
|
|
|
async fn move_object(
|
|
&self,
|
|
id: ServerId,
|
|
folder_id: Option<FolderId>,
|
|
owner: Owner,
|
|
object_type: ObjectType,
|
|
) -> Result<bool>;
|
|
|
|
async fn record_object_action(
|
|
&self,
|
|
id: ServerId,
|
|
action_type: ObjectActionType,
|
|
timestamp: DateTime<Utc>,
|
|
data: Option<String>,
|
|
) -> Result<ObjectActionHistory>;
|
|
|
|
async fn leave_object(&self, id: ServerId) -> Result<ObjectDeleteResult>;
|
|
|
|
async fn set_object_link_permissions(
|
|
&self,
|
|
object_id: ServerId,
|
|
access_level: SharingAccessLevel,
|
|
) -> Result<ObjectPermissionUpdateResult>;
|
|
|
|
async fn remove_object_link_permissions(
|
|
&self,
|
|
object_id: ServerId,
|
|
) -> Result<ObjectPermissionUpdateResult>;
|
|
|
|
async fn add_object_guests(
|
|
&self,
|
|
object_id: ServerId,
|
|
guest_emails: Vec<String>,
|
|
access_level: AccessLevel,
|
|
) -> Result<ObjectPermissionsUpdateData>;
|
|
|
|
async fn update_object_guests(
|
|
&self,
|
|
object_id: ServerId,
|
|
guest_emails: Vec<String>,
|
|
access_level: AccessLevel,
|
|
) -> Result<ServerPermissions>;
|
|
|
|
async fn remove_object_guest(
|
|
&self,
|
|
object_id: ServerId,
|
|
guest: GuestIdentifier,
|
|
) -> Result<ServerPermissions>;
|
|
|
|
/// Fetches the last-used timestamps for all cloud environments.
|
|
///
|
|
/// This is derived from `CloudEnvironment.lastTaskCreated.createdAt` (not `lastTaskRunTimestamp`)
|
|
/// so that "Last used" reflects the most recently created task.
|
|
///
|
|
/// Returns a map from environment UID to timestamp.
|
|
async fn fetch_environment_last_task_run_timestamps(
|
|
&self,
|
|
) -> Result<HashMap<String, DateTime<Utc>>>;
|
|
}
|
|
|
|
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
|
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
|
impl ObjectClient for ServerApi {
|
|
async fn create_workflow(
|
|
&self,
|
|
request: CreateObjectRequest,
|
|
) -> Result<CreateCloudObjectResult> {
|
|
let model = request
|
|
.serialized_model
|
|
.ok_or_else(|| anyhow!("missing model for creating workflow"))?;
|
|
let variables = CreateWorkflowVariables {
|
|
input: CreateWorkflowInput {
|
|
data: model.take(),
|
|
entrypoint: request.entrypoint.into(),
|
|
initial_folder_id: request.initial_folder_id.map(|folder_id| folder_id.into()),
|
|
owner: request.owner.into(),
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = CreateWorkflow::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
|
|
match response.create_workflow {
|
|
CreateWorkflowResult::CreateWorkflowOutput(output) => {
|
|
let metadata = output.workflow.metadata;
|
|
let workflow_id: WorkflowId = metadata.uid.into_inner().into();
|
|
|
|
Ok(CreateCloudObjectResult::Success {
|
|
created_cloud_object: CreatedCloudObject {
|
|
client_id: request.client_id,
|
|
revision_and_editor: RevisionAndLastEditor {
|
|
revision: output.revision_ts.into(),
|
|
last_editor_uid: metadata.last_editor_uid.map(|uid| uid.into_inner()),
|
|
},
|
|
metadata_ts: metadata.metadata_last_updated_ts,
|
|
server_id_and_type: ServerIdAndType {
|
|
id: workflow_id.to_server_id(),
|
|
id_type: ObjectIdType::Workflow,
|
|
},
|
|
creator_uid: metadata.creator_uid.map(|uid| uid.into_inner()),
|
|
permissions: output.workflow.permissions.try_into()?,
|
|
},
|
|
})
|
|
}
|
|
CreateWorkflowResult::UserFacingError(e) => Ok(
|
|
CreateCloudObjectResult::UserFacingError(get_user_facing_error_message(e)),
|
|
),
|
|
CreateWorkflowResult::Unknown => {
|
|
Err(anyhow!("Failed to create workflow due to unknown variant"))
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn update_workflow(
|
|
&self,
|
|
workflow_id: WorkflowId,
|
|
data: SerializedModel,
|
|
revision: Option<Revision>,
|
|
) -> Result<UpdateCloudObjectResult<ServerWorkflow>> {
|
|
let variables = UpdateWorkflowVariables {
|
|
input: UpdateWorkflowInput {
|
|
data: data.model_as_str().to_owned(),
|
|
uid: cynic::Id::new(workflow_id),
|
|
revision_ts: revision.map(|r| r.into()),
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = UpdateWorkflow::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
match response.update_workflow {
|
|
UpdateWorkflowResult::UpdateWorkflowOutput(output) => match output.update {
|
|
WorkflowUpdate::ObjectUpdateSuccess(success) => {
|
|
Ok(UpdateCloudObjectResult::Success {
|
|
revision_and_editor: RevisionAndLastEditor {
|
|
revision: success.revision_ts.into(),
|
|
last_editor_uid: Some(success.last_editor_uid.into_inner()),
|
|
},
|
|
})
|
|
}
|
|
WorkflowUpdate::WorkflowUpdateRejected(rejected) => {
|
|
Ok(UpdateCloudObjectResult::Rejected {
|
|
object: rejected.conflicting_workflow.try_into()?,
|
|
})
|
|
}
|
|
WorkflowUpdate::Unknown => Err(anyhow!("WorkflowUpdate has unknown variant")),
|
|
},
|
|
UpdateWorkflowResult::UserFacingError(e) => {
|
|
Err(anyhow!(get_user_facing_error_message(e)))
|
|
}
|
|
UpdateWorkflowResult::Unknown => {
|
|
Err(anyhow!("Failed to update workflow due to unknown variant"))
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn bulk_create_generic_string_objects(
|
|
&self,
|
|
owner: Owner,
|
|
objects: &[BulkCreateGenericStringObjectsRequest],
|
|
) -> Result<BulkCreateCloudObjectResult> {
|
|
let variables = BulkCreateObjectsVariables {
|
|
input: BulkCreateObjectsInput {
|
|
generic_string_objects: Some(BulkCreateGenericStringObjectsInput {
|
|
owner: owner.into(),
|
|
objects: objects
|
|
.iter()
|
|
.map(|object| GenericStringObjectInput {
|
|
client_id: cynic::Id::new(object.id.to_string()),
|
|
serialized_model: object.serialized_model.model_as_str().to_owned(),
|
|
format: object.format.into(),
|
|
uniqueness_key: object
|
|
.uniqueness_key
|
|
.clone()
|
|
.map(GenericStringObjectUniqueKey::into),
|
|
initial_folder_id: object.initial_folder_id.map(FolderId::into),
|
|
entrypoint: object.entrypoint.into(),
|
|
})
|
|
.collect(),
|
|
}),
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = BulkCreateObjects::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
match response.bulk_create_objects {
|
|
BulkCreateObjectsResult::BulkCreateObjectsOutput(output) => {
|
|
if let Some(gso_result) = output.generic_string_objects {
|
|
let mut created_cloud_objects = Vec::new();
|
|
for gso in gso_result.objects {
|
|
let metadata = gso.generic_string_object.metadata;
|
|
let uid = metadata.uid.into_inner();
|
|
let object_id: GenericStringObjectId = uid.into();
|
|
|
|
created_cloud_objects.push(CreatedCloudObject {
|
|
client_id: ClientId::from_hash(&gso.client_id.into_inner())
|
|
.ok_or_else(|| anyhow!("invalid client id"))?,
|
|
revision_and_editor: RevisionAndLastEditor {
|
|
revision: metadata.revision_ts.into(),
|
|
last_editor_uid: metadata
|
|
.last_editor_uid
|
|
.map(|uid| uid.into_inner()),
|
|
},
|
|
metadata_ts: metadata.metadata_last_updated_ts,
|
|
server_id_and_type: ServerIdAndType {
|
|
id: object_id.to_server_id(),
|
|
id_type: ObjectIdType::GenericStringObject,
|
|
},
|
|
creator_uid: metadata.creator_uid.map(|uid| uid.into_inner()),
|
|
permissions: gso.generic_string_object.permissions.try_into()?,
|
|
});
|
|
}
|
|
|
|
Ok(BulkCreateCloudObjectResult::Success {
|
|
created_cloud_objects,
|
|
})
|
|
} else {
|
|
Err(anyhow!(
|
|
"No generic string objects found in BulkCreateGenericStringObjectsOutput"
|
|
))
|
|
}
|
|
}
|
|
BulkCreateObjectsResult::UserFacingError(e) => match e.error {
|
|
UserFacingErrorInterface::GenericStringObjectUniqueKeyConflict(_) => {
|
|
Ok(BulkCreateCloudObjectResult::GenericStringObjectUniqueKeyConflict)
|
|
}
|
|
_ => Err(anyhow!(get_user_facing_error_message(e))),
|
|
},
|
|
BulkCreateObjectsResult::Unknown => Err(anyhow!(
|
|
"Failed to bulk create objects due to unknown variant"
|
|
)),
|
|
}
|
|
}
|
|
|
|
async fn create_generic_string_object(
|
|
&self,
|
|
format: GenericStringObjectFormat,
|
|
uniqueness_key: Option<GenericStringObjectUniqueKey>,
|
|
request: CreateObjectRequest,
|
|
) -> Result<CreateCloudObjectResult> {
|
|
let model = request
|
|
.serialized_model
|
|
.ok_or_else(|| anyhow!("missing model for creating generic string model"))?;
|
|
let variables = CreateGenericStringObjectVariables {
|
|
input: CreateGenericStringObjectInput {
|
|
generic_string_object: GenericStringObjectInput {
|
|
client_id: cynic::Id::new(request.client_id.to_hash()),
|
|
entrypoint: request.entrypoint.into(),
|
|
format: format.into(),
|
|
initial_folder_id: request.initial_folder_id.map(|folder_id| folder_id.into()),
|
|
serialized_model: model.take(),
|
|
uniqueness_key: uniqueness_key.map(|key| key.into()),
|
|
},
|
|
owner: request.owner.into(),
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = CreateGenericStringObject::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
|
|
match response.create_generic_string_object {
|
|
CreateGenericStringObjectResult::CreateGenericStringObjectOutput(output) => {
|
|
let metadata = output.generic_string_object.metadata;
|
|
let gso_id: GenericStringObjectId = metadata.uid.into_inner().into();
|
|
|
|
Ok(CreateCloudObjectResult::Success {
|
|
created_cloud_object: CreatedCloudObject {
|
|
revision_and_editor: RevisionAndLastEditor {
|
|
revision: output.revision_ts.into(),
|
|
last_editor_uid: metadata.last_editor_uid.map(|uid| uid.into_inner()),
|
|
},
|
|
metadata_ts: metadata.metadata_last_updated_ts,
|
|
server_id_and_type: ServerIdAndType {
|
|
id: gso_id.to_server_id(),
|
|
id_type: ObjectIdType::GenericStringObject,
|
|
},
|
|
creator_uid: metadata.creator_uid.map(|uid| uid.into_inner()),
|
|
client_id: request.client_id,
|
|
permissions: output.generic_string_object.permissions.try_into()?,
|
|
},
|
|
})
|
|
}
|
|
CreateGenericStringObjectResult::UserFacingError(e) => Ok(match e.error {
|
|
UserFacingErrorInterface::GenericStringObjectUniqueKeyConflict(_) => {
|
|
CreateCloudObjectResult::GenericStringObjectUniqueKeyConflict
|
|
}
|
|
_ => CreateCloudObjectResult::UserFacingError(get_user_facing_error_message(e)),
|
|
}),
|
|
CreateGenericStringObjectResult::Unknown => Err(anyhow!(
|
|
"Failed to create generic string object due to unknown variant"
|
|
)),
|
|
}
|
|
}
|
|
|
|
async fn create_notebook(
|
|
&self,
|
|
request: CreateObjectRequest,
|
|
) -> Result<CreateCloudObjectResult> {
|
|
let serialized = request
|
|
.serialized_model
|
|
.as_ref()
|
|
.ok_or_else(|| anyhow!("Missing serialized model for notebook"))?;
|
|
|
|
let notebook: SerializedNotebook = serde_json::from_str(serialized.model_as_str())
|
|
.context("Failed to deserialize notebook model")?;
|
|
|
|
let ai_document_id = notebook
|
|
.ai_document_id
|
|
.and_then(|id| AIDocumentId::try_from(id).ok());
|
|
|
|
let variables = CreateNotebookVariables {
|
|
input: CreateNotebookInput {
|
|
data: Some(notebook.data),
|
|
entrypoint: request.entrypoint.into(),
|
|
initial_folder_id: request.initial_folder_id.map(|folder_id| folder_id.into()),
|
|
owner: request.owner.into(),
|
|
title: request.title,
|
|
ai_document_id: ai_document_id.map(|id| id.to_string()),
|
|
conversation_id: notebook.conversation_id,
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = CreateNotebook::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
|
|
match response.create_notebook {
|
|
CreateNotebookResult::CreateNotebookOutput(output) => {
|
|
let metadata = output.notebook.metadata;
|
|
let notebook_id: NotebookId = metadata.uid.into_inner().into();
|
|
|
|
Ok(CreateCloudObjectResult::Success {
|
|
created_cloud_object: CreatedCloudObject {
|
|
client_id: request.client_id,
|
|
revision_and_editor: RevisionAndLastEditor {
|
|
revision: output.revision_ts.into(),
|
|
last_editor_uid: metadata.last_editor_uid.map(|uid| uid.into_inner()),
|
|
},
|
|
metadata_ts: metadata.metadata_last_updated_ts,
|
|
server_id_and_type: ServerIdAndType {
|
|
id: notebook_id.to_server_id(),
|
|
id_type: ObjectIdType::Notebook,
|
|
},
|
|
creator_uid: metadata.creator_uid.map(|uid| uid.into_inner()),
|
|
permissions: output.notebook.permissions.try_into()?,
|
|
},
|
|
})
|
|
}
|
|
CreateNotebookResult::UserFacingError(e) => Ok(
|
|
CreateCloudObjectResult::UserFacingError(get_user_facing_error_message(e)),
|
|
),
|
|
CreateNotebookResult::Unknown => {
|
|
Err(anyhow!("Failed to create notebook due to unknown variant"))
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn update_notebook(
|
|
&self,
|
|
notebook_id: NotebookId,
|
|
title: Option<String>,
|
|
data: Option<SerializedModel>,
|
|
revision: Option<Revision>,
|
|
) -> Result<UpdateCloudObjectResult<ServerNotebook>> {
|
|
let variables = UpdateNotebookVariables {
|
|
input: UpdateNotebookInput {
|
|
data: data.map(|data| data.model_as_str().to_owned()),
|
|
title,
|
|
uid: cynic::Id::new(notebook_id),
|
|
revision_ts: revision.map(|r| r.into()),
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = UpdateNotebook::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
match response.update_notebook {
|
|
UpdateNotebookResult::UpdateNotebookOutput(output) => match output.update {
|
|
NotebookUpdate::ObjectUpdateSuccess(success) => {
|
|
Ok(UpdateCloudObjectResult::Success {
|
|
revision_and_editor: RevisionAndLastEditor {
|
|
revision: success.revision_ts.into(),
|
|
last_editor_uid: Some(success.last_editor_uid.into_inner()),
|
|
},
|
|
})
|
|
}
|
|
NotebookUpdate::NotebookUpdateRejected(rejected) => {
|
|
Ok(UpdateCloudObjectResult::Rejected {
|
|
object: rejected.conflicting_notebook.try_into()?,
|
|
})
|
|
}
|
|
NotebookUpdate::Unknown => Err(anyhow!("NotebookUpdate has unknown variant")),
|
|
},
|
|
UpdateNotebookResult::UserFacingError(e) => {
|
|
Err(anyhow!(get_user_facing_error_message(e)))
|
|
}
|
|
UpdateNotebookResult::Unknown => {
|
|
Err(anyhow!("Failed to update notebook due to unknown variant"))
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn create_folder(&self, request: CreateObjectRequest) -> Result<CreateCloudObjectResult> {
|
|
let model = request
|
|
.serialized_model
|
|
.ok_or_else(|| anyhow!("missing serialized model for creating folder"))?;
|
|
let variables = CreateFolderVariables {
|
|
input: CreateFolderInput {
|
|
initial_folder_id: request.initial_folder_id.map(|folder_id| folder_id.into()),
|
|
name: model.take(),
|
|
owner: request.owner.into(),
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = CreateFolder::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
|
|
match response.create_folder {
|
|
CreateFolderResult::CreateFolderOutput(output) => {
|
|
let metadata = output.folder.metadata;
|
|
let folder_id: FolderId = metadata.uid.into_inner().into();
|
|
|
|
Ok(CreateCloudObjectResult::Success {
|
|
created_cloud_object: CreatedCloudObject {
|
|
client_id: request.client_id,
|
|
revision_and_editor: RevisionAndLastEditor {
|
|
revision: metadata.revision_ts.into(),
|
|
last_editor_uid: metadata.last_editor_uid.map(|uid| uid.into_inner()),
|
|
},
|
|
metadata_ts: metadata.metadata_last_updated_ts,
|
|
server_id_and_type: ServerIdAndType {
|
|
id: folder_id.to_server_id(),
|
|
id_type: ObjectIdType::Folder,
|
|
},
|
|
creator_uid: metadata.creator_uid.map(|uid| uid.into_inner()),
|
|
permissions: output.folder.permissions.try_into()?,
|
|
},
|
|
})
|
|
}
|
|
CreateFolderResult::UserFacingError(e) => Ok(CreateCloudObjectResult::UserFacingError(
|
|
get_user_facing_error_message(e),
|
|
)),
|
|
CreateFolderResult::Unknown => {
|
|
Err(anyhow!("Failed to create folder due to unknown variant"))
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn update_folder(
|
|
&self,
|
|
folder_id: FolderId,
|
|
name: SerializedModel,
|
|
) -> Result<UpdateCloudObjectResult<ServerFolder>> {
|
|
let variables = UpdateFolderVariables {
|
|
input: UpdateFolderInput {
|
|
uid: cynic::Id::new(folder_id),
|
|
name: name.model_as_str().to_owned(),
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = UpdateFolder::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
|
|
match response.update_folder {
|
|
UpdateFolderResult::UpdateFolderOutput(output) => output.update.try_into(),
|
|
UpdateFolderResult::UserFacingError(e) => {
|
|
Err(anyhow!(get_user_facing_error_message(e)))
|
|
}
|
|
UpdateFolderResult::Unknown => {
|
|
Err(anyhow!("Failed to update folder due to unknown variant"))
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn update_generic_string_object(
|
|
&self,
|
|
object_id: GenericStringObjectId,
|
|
model: SerializedModel,
|
|
revision: Option<Revision>,
|
|
) -> Result<UpdateCloudObjectResult<Box<dyn ServerObject>>> {
|
|
let variables = UpdateGenericStringObjectVariables {
|
|
input: UpdateGenericStringObjectInput {
|
|
revision_ts: revision.map(|r| r.into()),
|
|
serialized_model: model.model_as_str().to_owned(),
|
|
uid: object_id.into(),
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = UpdateGenericStringObject::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
response.update_generic_string_object.try_into()
|
|
}
|
|
|
|
async fn grab_notebook_edit_access(&self, notebook_id: NotebookId) -> Result<ServerMetadata> {
|
|
let variables = GrabNotebookEditAccessVariables {
|
|
input: UpdateNotebookEditAccessInput {
|
|
uid: cynic::Id::new(notebook_id),
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = GrabNotebookEditAccess::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
|
|
match response.grab_notebook_edit_access {
|
|
UpdateNotebookEditAccessResult::UpdateNotebookEditAccessOutput(output) => {
|
|
// The grabNotebookEditAccess API errors if unable to grab the baton,
|
|
// so we're always in the success case here.
|
|
output.metadata.try_into()
|
|
}
|
|
UpdateNotebookEditAccessResult::UserFacingError(e) => {
|
|
Err(anyhow!(get_user_facing_error_message(e)))
|
|
}
|
|
UpdateNotebookEditAccessResult::Unknown => Err(anyhow!(
|
|
"Failed to grab notebook edit access due to unknown variant"
|
|
)),
|
|
}
|
|
}
|
|
|
|
async fn give_up_notebook_edit_access(
|
|
&self,
|
|
notebook_id: NotebookId,
|
|
) -> Result<ServerMetadata> {
|
|
let variables = GiveUpNotebookEditAccessVariables {
|
|
input: UpdateNotebookEditAccessInput {
|
|
uid: cynic::Id::new(notebook_id),
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = GiveUpNotebookEditAccess::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
|
|
match response.give_up_notebook_edit_access {
|
|
UpdateNotebookEditAccessResult::UpdateNotebookEditAccessOutput(output) => {
|
|
output.metadata.try_into()
|
|
}
|
|
UpdateNotebookEditAccessResult::UserFacingError(e) => {
|
|
Err(anyhow!(get_user_facing_error_message(e)))
|
|
}
|
|
UpdateNotebookEditAccessResult::Unknown => Err(anyhow!(
|
|
"Failed to give up notebook edit access due to unknown variant"
|
|
)),
|
|
}
|
|
}
|
|
|
|
/// Starts a websocket connections against the corresponding GraphQL subscription.
|
|
/// Messages received over the socket are sent over the `message_sender`.
|
|
/// Once the websocket is live, a one-shot message is sent over `stream_ready_sender`
|
|
/// to indicate so. This is because this method only returns once the websocket is closed.
|
|
async fn get_warp_drive_updates(
|
|
&self,
|
|
message_sender: Sender<ObjectUpdateMessage>,
|
|
stream_ready_sender: Sender<()>,
|
|
) -> Result<()> {
|
|
// The init payload is how we convey any metadata about
|
|
// the subscription to the server (i.e. in lieu of http headers).
|
|
// TODO (written by Suraj): we should consider consolidating the places we
|
|
// supply this common data. GQL subscriptions use a different
|
|
// implementation from our general server requests (which make
|
|
// use of [`crate::http`]).
|
|
let mut init_payload = HashMap::new();
|
|
|
|
// Add the bearer token to the init payload when using header-based auth.
|
|
// Session-cookie-authenticated clients rely on the websocket handshake cookies instead.
|
|
let auth_token = self.get_or_refresh_access_token().await?;
|
|
if let Some(token) = auth_token.as_bearer_token() {
|
|
let bearer_token = format!("Bearer {token}");
|
|
init_payload.insert(http_client::AUTHORIZATION.as_str(), bearer_token);
|
|
}
|
|
|
|
// Add the app version, if available.
|
|
if let Some(app_version) = ChannelState::app_version() {
|
|
init_payload.insert(
|
|
http_client::headers::CLIENT_RELEASE_VERSION_HEADER_KEY,
|
|
app_version.to_string(),
|
|
);
|
|
}
|
|
|
|
let subscription = GetWarpDriveUpdates::build(());
|
|
|
|
start_graphql_streaming_operation(
|
|
&ChannelState::ws_server_url(),
|
|
init_payload,
|
|
subscription,
|
|
|res| {
|
|
res.ok_or_else(|| {
|
|
anyhow!("missing response data for message in get_warp_drive_updates")
|
|
})
|
|
.and_then(|data| data.warp_drive_updates.try_into())
|
|
},
|
|
message_sender,
|
|
stream_ready_sender,
|
|
)
|
|
.await
|
|
}
|
|
|
|
async fn fetch_changed_objects(
|
|
&self,
|
|
objects_to_update: ObjectsToUpdate,
|
|
force_refresh: bool,
|
|
) -> Result<InitialLoadResponse> {
|
|
log::info!("fetching updated cloud objects");
|
|
if force_refresh {
|
|
log::info!("forcing sync of all objects")
|
|
}
|
|
|
|
let variables = GetUpdatedCloudObjectsVariables {
|
|
input: UpdatedCloudObjectsInput {
|
|
folders: Some(objects_to_update.folders),
|
|
force_refresh,
|
|
generic_string_objects: Some(objects_to_update.generic_string_objects),
|
|
notebooks: Some(objects_to_update.notebooks),
|
|
workflows: Some(objects_to_update.workflows),
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = GetUpdatedCloudObjects::build(variables);
|
|
let response_data = self.send_graphql_request(operation, None).await?;
|
|
|
|
match response_data.updated_cloud_objects {
|
|
UpdatedCloudObjectsResult::UpdatedCloudObjectsOutput(output) => {
|
|
let updated_notebooks = output
|
|
.notebooks
|
|
.map(|notebooks| {
|
|
notebooks
|
|
.into_iter()
|
|
.filter_map(|notebook| {
|
|
ServerNotebook::try_from_graphql_fields(
|
|
ServerId::from_string_lossy(notebook.metadata.uid.inner()),
|
|
Some(notebook.title),
|
|
Some(notebook.data),
|
|
notebook.ai_document_id,
|
|
notebook.metadata.try_into().ok()?,
|
|
notebook.permissions.try_into().ok()?,
|
|
)
|
|
.ok()
|
|
})
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let updated_workflows = output
|
|
.workflows
|
|
.map(|workflows| {
|
|
workflows
|
|
.into_iter()
|
|
.filter_map(|workflow| {
|
|
ServerWorkflow::try_from_graphql_fields(
|
|
ServerId::from_string_lossy(workflow.metadata.uid.inner()),
|
|
workflow.data,
|
|
workflow.metadata.try_into().ok()?,
|
|
workflow.permissions.try_into().ok()?,
|
|
)
|
|
.ok()
|
|
})
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let updated_folders = output
|
|
.folders
|
|
.map(|folders| {
|
|
folders
|
|
.into_iter()
|
|
.filter_map(|folder| {
|
|
ServerFolder::try_from_graphql_fields(
|
|
ServerId::from_string_lossy(folder.metadata.uid.inner()),
|
|
Some(folder.name),
|
|
folder.metadata.try_into().ok()?,
|
|
folder.permissions.try_into().ok()?,
|
|
folder.is_warp_pack,
|
|
)
|
|
.ok()
|
|
})
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let mut updated_generic_string_objects = HashMap::new();
|
|
if let Some(objects) = output.generic_string_objects {
|
|
for gso in objects {
|
|
let uid = gso.metadata.uid.inner().to_string();
|
|
let server_id = ServerId::from_string_lossy(&uid);
|
|
|
|
let metadata = match ServerMetadata::try_from(gso.metadata) {
|
|
Ok(metadata) => metadata,
|
|
Err(err) => {
|
|
report_error!(err.context(format!(
|
|
"Failed to convert metadata for GSO {:?} {uid}",
|
|
gso.format
|
|
)));
|
|
continue;
|
|
}
|
|
};
|
|
|
|
let permissions = match ServerPermissions::try_from(gso.permissions) {
|
|
Ok(permissions) => permissions,
|
|
Err(err) => {
|
|
report_error!(err.context(format!(
|
|
"Failed to convert permissions for GSO {:?} {uid}",
|
|
gso.format
|
|
)));
|
|
continue;
|
|
}
|
|
};
|
|
|
|
match gso.format {
|
|
warp_graphql::generic_string_object::GenericStringObjectFormat::JsonEnvVarCollection => {
|
|
parse_server_gso::<EnvVarCollection, JsonSerializer>(
|
|
&mut updated_generic_string_objects,
|
|
GenericStringObjectFormat::Json(JsonObjectType::EnvVarCollection),
|
|
server_id,
|
|
metadata,
|
|
permissions,
|
|
gso.serialized_model,
|
|
);
|
|
}
|
|
warp_graphql::generic_string_object::GenericStringObjectFormat::JsonPreference => {
|
|
parse_server_gso::<Preference, JsonSerializer>(
|
|
&mut updated_generic_string_objects,
|
|
GenericStringObjectFormat::Json(JsonObjectType::Preference),
|
|
server_id,
|
|
metadata,
|
|
permissions,
|
|
gso.serialized_model,
|
|
);
|
|
}
|
|
warp_graphql::generic_string_object::GenericStringObjectFormat::JsonWorkflowEnum => {
|
|
parse_server_gso::<WorkflowEnum, JsonSerializer>(
|
|
&mut updated_generic_string_objects,
|
|
GenericStringObjectFormat::Json(JsonObjectType::WorkflowEnum),
|
|
server_id,
|
|
metadata,
|
|
permissions,
|
|
gso.serialized_model,
|
|
);
|
|
}
|
|
warp_graphql::generic_string_object::GenericStringObjectFormat::JsonAIFact => {
|
|
parse_server_gso::<AIFact, JsonSerializer>(
|
|
&mut updated_generic_string_objects,
|
|
GenericStringObjectFormat::Json(JsonObjectType::AIFact),
|
|
server_id,
|
|
metadata,
|
|
permissions,
|
|
gso.serialized_model,
|
|
);
|
|
}
|
|
warp_graphql::generic_string_object::GenericStringObjectFormat::JsonMCPServer => {
|
|
parse_server_gso::<MCPServer, JsonSerializer>(
|
|
&mut updated_generic_string_objects,
|
|
GenericStringObjectFormat::Json(JsonObjectType::MCPServer),
|
|
server_id,
|
|
metadata,
|
|
permissions,
|
|
gso.serialized_model,
|
|
);
|
|
}
|
|
warp_graphql::generic_string_object::GenericStringObjectFormat::JsonAIExecutionProfile => {
|
|
parse_server_gso::<AIExecutionProfile, JsonSerializer>(
|
|
&mut updated_generic_string_objects,
|
|
GenericStringObjectFormat::Json(JsonObjectType::AIExecutionProfile),
|
|
server_id,
|
|
metadata,
|
|
permissions,
|
|
gso.serialized_model,
|
|
);
|
|
}
|
|
warp_graphql::generic_string_object::GenericStringObjectFormat::JsonTemplatableMCPServer => {
|
|
parse_server_gso::<TemplatableMCPServer, JsonSerializer>(
|
|
&mut updated_generic_string_objects,
|
|
GenericStringObjectFormat::Json(JsonObjectType::TemplatableMCPServer),
|
|
server_id,
|
|
metadata,
|
|
permissions,
|
|
gso.serialized_model,
|
|
);
|
|
}
|
|
warp_graphql::generic_string_object::GenericStringObjectFormat::JsonCloudEnvironment => {
|
|
parse_server_gso::<AmbientAgentEnvironment, JsonSerializer>(
|
|
&mut updated_generic_string_objects,
|
|
GenericStringObjectFormat::Json(JsonObjectType::CloudEnvironment),
|
|
server_id,
|
|
metadata,
|
|
permissions,
|
|
gso.serialized_model,
|
|
);
|
|
}
|
|
warp_graphql::generic_string_object::GenericStringObjectFormat::JsonScheduledAmbientAgent => {
|
|
parse_server_gso::<ScheduledAmbientAgent, JsonSerializer>(
|
|
&mut updated_generic_string_objects,
|
|
GenericStringObjectFormat::Json(JsonObjectType::ScheduledAmbientAgent),
|
|
server_id,
|
|
metadata,
|
|
permissions,
|
|
gso.serialized_model,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let deleted_notebooks: Vec<NotebookId> = output
|
|
.deleted_object_uids
|
|
.notebook_uids
|
|
.map(|uids| {
|
|
uids.into_iter()
|
|
.map(|uid| uid.into_inner().into())
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let deleted_workflows: Vec<WorkflowId> = output
|
|
.deleted_object_uids
|
|
.workflow_uids
|
|
.map(|uids| {
|
|
uids.into_iter()
|
|
.map(|uid| uid.into_inner().into())
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let deleted_folders: Vec<FolderId> = output
|
|
.deleted_object_uids
|
|
.folder_uids
|
|
.map(|uids| {
|
|
uids.into_iter()
|
|
.map(|uid| uid.into_inner().into())
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let deleted_generic_string_objects: Vec<GenericStringObjectId> = output
|
|
.deleted_object_uids
|
|
.generic_string_object_uids
|
|
.map(|uids| {
|
|
uids.into_iter()
|
|
.map(|uid| uid.into_inner().into())
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let user_profiles: Vec<UserProfileWithUID> = output
|
|
.user_profiles
|
|
.map(|user_profiles| {
|
|
user_profiles
|
|
.into_iter()
|
|
.map(|profile| profile.into())
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let action_histories: Vec<ObjectActionHistory> = output
|
|
.action_histories
|
|
.map(|histories| {
|
|
histories
|
|
.into_iter()
|
|
.filter_map(|history| history.try_into().ok())
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let mcp_gallery = output.mcp_gallery.unwrap_or_default();
|
|
|
|
let response = InitialLoadResponse {
|
|
updated_notebooks,
|
|
deleted_notebooks,
|
|
updated_workflows,
|
|
deleted_workflows,
|
|
updated_folders,
|
|
deleted_folders,
|
|
updated_generic_string_objects,
|
|
deleted_generic_string_objects,
|
|
user_profiles,
|
|
action_histories,
|
|
mcp_gallery,
|
|
};
|
|
Ok(response)
|
|
}
|
|
UpdatedCloudObjectsResult::UserFacingError(e) => {
|
|
Err(anyhow!(get_user_facing_error_message(e)))
|
|
}
|
|
UpdatedCloudObjectsResult::Unknown => Err(anyhow!(
|
|
"Failed to get updated cloud objects due to unknown variant"
|
|
)),
|
|
}
|
|
}
|
|
|
|
async fn fetch_single_cloud_object(&self, id: ServerId) -> Result<GetCloudObjectResponse> {
|
|
let variables = GetCloudObjectVariables {
|
|
input: CloudObjectInput {
|
|
uid: cynic::Id::new(id),
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = GetCloudObject::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
match response.cloud_object {
|
|
CloudObjectResult::CloudObjectOutput(output) => {
|
|
let object: ServerCloudObject = output.object.clone().try_into()?;
|
|
let action_histories: Vec<ObjectActionHistory> = output
|
|
.action_histories
|
|
.map(|histories| {
|
|
histories
|
|
.into_iter()
|
|
.filter_map(|history| history.try_into().ok())
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
let descendants = match output.object {
|
|
CloudObjectWithDescendants::FolderWithDescendants(folder) => folder
|
|
.descendants
|
|
.into_iter()
|
|
.filter_map(|descendant| descendant.try_into().ok())
|
|
.collect(),
|
|
_ => vec![],
|
|
};
|
|
|
|
Ok(GetCloudObjectResponse {
|
|
object,
|
|
action_histories,
|
|
descendants,
|
|
})
|
|
}
|
|
CloudObjectResult::UserFacingError(e) => Err(anyhow!(get_user_facing_error_message(e))),
|
|
CloudObjectResult::Unknown => Err(anyhow!(
|
|
"Failed to fetch single cloud object due to unknown variant"
|
|
)),
|
|
}
|
|
}
|
|
|
|
async fn transfer_notebook_owner(&self, notebook_id: NotebookId, owner: Owner) -> Result<bool> {
|
|
let variables = TransferNotebookOwnerVariables {
|
|
input: TransferNotebookOwnerInput {
|
|
uid: cynic::Id::new(notebook_id),
|
|
owner: owner.into(),
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = TransferNotebookOwner::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
match response.transfer_notebook_owner {
|
|
TransferNotebookOwnerResult::TransferNotebookOwnerOutput(output) => Ok(output.success),
|
|
_ => Ok(false),
|
|
}
|
|
}
|
|
|
|
async fn transfer_workflow_owner(&self, workflow_id: WorkflowId, owner: Owner) -> Result<bool> {
|
|
let variables = TransferWorkflowOwnerVariables {
|
|
input: TransferWorkflowOwnerInput {
|
|
uid: cynic::Id::new(workflow_id),
|
|
owner: owner.into(),
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = TransferWorkflowOwner::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
match response.transfer_workflow_owner {
|
|
TransferWorkflowOwnerResult::TransferWorkflowOwnerOutput(output) => Ok(output.success),
|
|
_ => Ok(false),
|
|
}
|
|
}
|
|
|
|
async fn transfer_generic_string_object_owner(
|
|
&self,
|
|
gso_id: GenericStringObjectId,
|
|
owner: Owner,
|
|
) -> Result<bool> {
|
|
let variables = TransferGenericStringObjectOwnerVariables {
|
|
input: TransferGenericStringObjectOwnerInput {
|
|
uid: cynic::Id::new(gso_id),
|
|
owner: owner.into(),
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = TransferGenericStringObjectOwner::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
match response.transfer_generic_string_object_owner {
|
|
TransferGenericStringObjectOwnerResult::TransferGenericStringObjectOwnerOutput(
|
|
output,
|
|
) => Ok(output.success),
|
|
_ => Ok(false),
|
|
}
|
|
}
|
|
|
|
async fn trash_object(&self, id: ServerId) -> Result<bool> {
|
|
let variables = TrashObjectVariables {
|
|
input: TrashObjectInput {
|
|
uid: cynic::Id::new(id),
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = TrashObject::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
let success = match response.trash_object {
|
|
TrashObjectResult::TrashObjectOutput(output) => output.success,
|
|
_ => false,
|
|
};
|
|
Ok(success)
|
|
}
|
|
|
|
async fn untrash_object(&self, id: ServerId) -> Result<ObjectMetadataUpdateResult> {
|
|
let variables = UntrashObjectVariables {
|
|
input: UntrashObjectInput {
|
|
uid: cynic::Id::new(id),
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = UntrashObject::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
|
|
let result = match response.untrash_object {
|
|
warp_graphql::mutations::untrash_object::UntrashObjectResult::UntrashObjectOutput(
|
|
output,
|
|
) => {
|
|
if output.success {
|
|
ObjectMetadataUpdateResult::Success {
|
|
metadata: Box::new(output.metadata.try_into()?),
|
|
}
|
|
} else {
|
|
ObjectMetadataUpdateResult::Failure
|
|
}
|
|
}
|
|
_ => ObjectMetadataUpdateResult::Failure,
|
|
};
|
|
Ok(result)
|
|
}
|
|
|
|
async fn delete_object(&self, id: ServerId) -> Result<ObjectDeleteResult> {
|
|
let variables = DeleteObjectVariables {
|
|
input: DeleteObjectInput {
|
|
uid: cynic::Id::new(id),
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = DeleteObject::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
let status = match response.delete_object {
|
|
DeleteObjectResult::DeleteObjectOutput(output) => {
|
|
let mut deleted_ids: Vec<SyncId> = Vec::new();
|
|
for uid in output.deleted_uids {
|
|
deleted_ids.push(SyncId::ServerId(ServerId::from_string_lossy(uid.inner())))
|
|
}
|
|
ObjectDeleteResult::Success { deleted_ids }
|
|
}
|
|
_ => ObjectDeleteResult::Failure,
|
|
};
|
|
Ok(status)
|
|
}
|
|
|
|
async fn empty_trash(&self, owner: Owner) -> Result<ObjectDeleteResult> {
|
|
let variables = EmptyTrashVariables {
|
|
input: EmptyTrashInput {
|
|
owner: owner.into(),
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = EmptyTrash::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
let status = match response.empty_trash {
|
|
EmptyTrashResult::EmptyTrashOutput(output) => {
|
|
let mut deleted_ids: Vec<SyncId> = Vec::new();
|
|
for uid in output.deleted_uids {
|
|
deleted_ids.push(SyncId::ServerId(ServerId::from_string_lossy(
|
|
uid.into_inner(),
|
|
)))
|
|
}
|
|
ObjectDeleteResult::Success { deleted_ids }
|
|
}
|
|
_ => ObjectDeleteResult::Failure,
|
|
};
|
|
Ok(status)
|
|
}
|
|
|
|
async fn move_object(
|
|
&self,
|
|
id: ServerId,
|
|
folder_id: Option<FolderId>,
|
|
owner: Owner,
|
|
object_type: ObjectType,
|
|
) -> Result<bool> {
|
|
let variables = MoveObjectVariables {
|
|
input: MoveObjectInput {
|
|
new_folder_uid: folder_id.map(cynic::Id::new),
|
|
new_owner: owner.into(),
|
|
object_type: object_type.into(),
|
|
uid: cynic::Id::new(id),
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = MoveObject::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
match response.move_object {
|
|
MoveObjectResult::MoveObjectOutput(output) => Ok(output.success),
|
|
MoveObjectResult::UserFacingError(e) => Err(anyhow!(get_user_facing_error_message(e))),
|
|
MoveObjectResult::Unknown => {
|
|
Err(anyhow!("Failed to move object due to unknown variant"))
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn record_object_action(
|
|
&self,
|
|
id: ServerId,
|
|
action_type: ObjectActionType,
|
|
timestamp: DateTime<Utc>,
|
|
data: Option<String>,
|
|
) -> Result<ObjectActionHistory> {
|
|
let variables = RecordObjectActionVariables {
|
|
input: RecordObjectActionInput {
|
|
action: action_type.into(),
|
|
json_data: data,
|
|
timestamp: timestamp.into(),
|
|
uid: id.into(),
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = RecordObjectAction::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
match response.record_object_action {
|
|
RecordObjectActionResult::RecordObjectActionOutput(output) => output.history.try_into(),
|
|
RecordObjectActionResult::UserFacingError(e) => {
|
|
Err(anyhow!(get_user_facing_error_message(e)))
|
|
}
|
|
RecordObjectActionResult::Unknown => Err(anyhow!(
|
|
"Failed to record object action due to unknown variant"
|
|
)),
|
|
}
|
|
}
|
|
|
|
async fn leave_object(&self, id: ServerId) -> Result<ObjectDeleteResult> {
|
|
let variables = LeaveObjectVariables {
|
|
input: LeaveObjectInput {
|
|
object_uid: id.into(),
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = LeaveObject::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
match response.leave_object {
|
|
LeaveObjectResult::LeaveObjectOutput(output) => Ok(ObjectDeleteResult::Success {
|
|
deleted_ids: vec![SyncId::ServerId(ServerId::from_string_lossy(
|
|
output.object_uid.into_inner(),
|
|
))],
|
|
}),
|
|
LeaveObjectResult::UserFacingError(e) => Err(anyhow!(get_user_facing_error_message(e))),
|
|
LeaveObjectResult::Unknown => Err(anyhow!("Unknown variant leaving object")),
|
|
}
|
|
}
|
|
|
|
async fn set_object_link_permissions(
|
|
&self,
|
|
object_id: ServerId,
|
|
access_level: SharingAccessLevel,
|
|
) -> Result<ObjectPermissionUpdateResult> {
|
|
let variables = SetObjectLinkPermissionsVariables {
|
|
input: SetObjectLinkPermissionsInput {
|
|
uid: cynic::Id::new(object_id),
|
|
access_level: access_level.into(),
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = SetObjectLinkPermissions::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
match response.set_object_link_permissions {
|
|
SetObjectLinkPermissionsResult::SetObjectLinkPermissionsOutput(_) => {
|
|
Ok(ObjectPermissionUpdateResult::Success)
|
|
}
|
|
SetObjectLinkPermissionsResult::UserFacingError(e) => {
|
|
Err(anyhow!(get_user_facing_error_message(e)))
|
|
}
|
|
SetObjectLinkPermissionsResult::Unknown => Err(anyhow!(
|
|
"Failed to set object link permissions due to unknown variant"
|
|
)),
|
|
}
|
|
}
|
|
|
|
async fn remove_object_link_permissions(
|
|
&self,
|
|
object_id: ServerId,
|
|
) -> Result<ObjectPermissionUpdateResult> {
|
|
let variables = RemoveObjectLinkPermissionsVariables {
|
|
input: RemoveObjectLinkPermissionsInput {
|
|
uid: cynic::Id::new(object_id),
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = RemoveObjectLinkPermissions::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
match response.remove_object_link_permissions {
|
|
RemoveObjectLinkPermissionsResult::RemoveObjectLinkPermissionsOutput(_) => {
|
|
Ok(ObjectPermissionUpdateResult::Success)
|
|
}
|
|
RemoveObjectLinkPermissionsResult::UserFacingError(e) => {
|
|
Err(anyhow!(get_user_facing_error_message(e)))
|
|
}
|
|
RemoveObjectLinkPermissionsResult::Unknown => Err(anyhow!(
|
|
"Failed to remove object link permissions due to unknown variant"
|
|
)),
|
|
}
|
|
}
|
|
|
|
async fn add_object_guests(
|
|
&self,
|
|
object_id: ServerId,
|
|
guest_emails: Vec<String>,
|
|
access_level: AccessLevel,
|
|
) -> Result<ObjectPermissionsUpdateData> {
|
|
let variables = AddObjectGuestsVariables {
|
|
input: AddObjectGuestsInput {
|
|
object_uid: cynic::Id::new(object_id),
|
|
access_level,
|
|
user_emails: guest_emails,
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = AddObjectGuests::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
|
|
match response.add_object_guests {
|
|
AddObjectGuestsResult::AddObjectGuestsOutput(output) => {
|
|
let permissions = output.object_permissions.try_into()?;
|
|
let profiles = output
|
|
.user_profiles
|
|
.into_iter()
|
|
.flatten()
|
|
.map(Into::into)
|
|
.collect();
|
|
Ok(ObjectPermissionsUpdateData {
|
|
permissions,
|
|
profiles,
|
|
})
|
|
}
|
|
AddObjectGuestsResult::UserFacingError(e) => {
|
|
Err(anyhow!(get_user_facing_error_message(e)))
|
|
}
|
|
AddObjectGuestsResult::Unknown => Err(anyhow!(
|
|
"Failed to add object guests due to unknown variant"
|
|
)),
|
|
}
|
|
}
|
|
|
|
async fn update_object_guests(
|
|
&self,
|
|
object_id: ServerId,
|
|
guest_emails: Vec<String>,
|
|
access_level: AccessLevel,
|
|
) -> Result<ServerPermissions> {
|
|
let variables = UpdateObjectGuestsVariables {
|
|
input: UpdateObjectGuestsInput {
|
|
object_uid: cynic::Id::new(object_id),
|
|
access_level,
|
|
emails: Some(guest_emails),
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = UpdateObjectGuests::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
|
|
match response.update_object_guests {
|
|
UpdateObjectGuestsResult::UpdateObjectGuestsOutput(output) => {
|
|
Ok(output.object_permissions.try_into()?)
|
|
}
|
|
UpdateObjectGuestsResult::UserFacingError(e) => {
|
|
Err(anyhow!(get_user_facing_error_message(e)))
|
|
}
|
|
UpdateObjectGuestsResult::Unknown => Err(anyhow!(
|
|
"Failed to update object guests due to unknown variant"
|
|
)),
|
|
}
|
|
}
|
|
|
|
async fn remove_object_guest(
|
|
&self,
|
|
object_id: ServerId,
|
|
guest: GuestIdentifier,
|
|
) -> Result<ServerPermissions> {
|
|
let (email, team_uid) = match guest {
|
|
GuestIdentifier::Email(email) => (Some(email), None),
|
|
GuestIdentifier::TeamUid(uid) => (None, Some(cynic::Id::new(uid))),
|
|
};
|
|
|
|
let variables = RemoveObjectGuestVariables {
|
|
input: RemoveObjectGuestInput {
|
|
email,
|
|
object_uid: cynic::Id::new(object_id),
|
|
team_uid,
|
|
},
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = RemoveObjectGuest::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
|
|
match response.remove_object_guest {
|
|
RemoveObjectGuestResult::RemoveObjectGuestOutput(output) => {
|
|
Ok(output.object_permissions.try_into()?)
|
|
}
|
|
RemoveObjectGuestResult::UserFacingError(e) => {
|
|
Err(anyhow!(get_user_facing_error_message(e)))
|
|
}
|
|
RemoveObjectGuestResult::Unknown => Err(anyhow!(
|
|
"Failed to remove object guest due to unknown variant"
|
|
)),
|
|
}
|
|
}
|
|
|
|
async fn fetch_environment_last_task_run_timestamps(
|
|
&self,
|
|
) -> Result<HashMap<String, DateTime<Utc>>> {
|
|
let variables = GetCloudEnvironmentsQueryVariables {
|
|
request_context: get_request_context(),
|
|
};
|
|
|
|
let operation = GetCloudEnvironmentsQuery::build(variables);
|
|
let response = self.send_graphql_request(operation, None).await?;
|
|
|
|
match response.get_cloud_environments {
|
|
GetCloudEnvironmentsResult::GetCloudEnvironmentsOutput(output) => {
|
|
let mut timestamps = HashMap::new();
|
|
for env in output.cloud_environments {
|
|
if let Some(task) = env.last_task_created {
|
|
timestamps.insert(env.uid.into_inner(), task.created_at.utc());
|
|
}
|
|
}
|
|
Ok(timestamps)
|
|
}
|
|
GetCloudEnvironmentsResult::UserFacingError(e) => {
|
|
Err(anyhow!(get_user_facing_error_message(e)))
|
|
}
|
|
GetCloudEnvironmentsResult::Unknown => Err(anyhow!(
|
|
"Failed to fetch cloud environments due to unknown variant"
|
|
)),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Parse the serialized model for a GSO and add it to the format-specific entry in `map`,
|
|
/// or report an error if parsing fails.
|
|
fn parse_server_gso<T, S>(
|
|
map: &mut HashMap<GenericStringObjectFormat, Vec<Box<dyn ServerObject>>>,
|
|
format: GenericStringObjectFormat,
|
|
uid: ServerId,
|
|
metadata: ServerMetadata,
|
|
permissions: ServerPermissions,
|
|
serialized_model: String,
|
|
) where
|
|
T: StringModel<
|
|
CloudObjectType = GenericCloudObject<GenericStringObjectId, GenericStringModel<T, S>>,
|
|
>,
|
|
S: Serializer<T>,
|
|
{
|
|
match GenericServerObject::<GenericStringObjectId, GenericStringModel<T, S>>::try_from_graphql_fields(uid, Some(serialized_model), metadata, permissions)
|
|
{
|
|
Ok(object) => {
|
|
map.entry(format).or_default().push(Box::new(object));
|
|
}
|
|
Err(err) => report_error!(err.context(format!("Failed to convert {format:?} {uid}"))),
|
|
}
|
|
}
|