first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
use warp_graphql::scalars::time::ServerTimestamp;
|
||||
|
||||
use super::{
|
||||
CloudObjectEventEntrypoint, GenericStringObjectFormat, GenericStringObjectUniqueKey, Owner,
|
||||
RevisionAndLastEditor, SerializedModel, ServerPermissions,
|
||||
};
|
||||
use crate::ids::{ClientId, FolderId, ServerIdAndType};
|
||||
|
||||
/// Helper struct that contains all the info needed to create an object on the server.
|
||||
pub struct CreateObjectRequest {
|
||||
pub serialized_model: Option<SerializedModel>,
|
||||
pub title: Option<String>,
|
||||
pub owner: Owner,
|
||||
pub client_id: ClientId,
|
||||
pub initial_folder_id: Option<FolderId>,
|
||||
pub entrypoint: CloudObjectEventEntrypoint,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub struct BulkCreateGenericStringObjectsRequest {
|
||||
pub id: ClientId,
|
||||
pub format: GenericStringObjectFormat,
|
||||
pub uniqueness_key: Option<GenericStringObjectUniqueKey>,
|
||||
pub serialized_model: SerializedModel,
|
||||
pub initial_folder_id: Option<FolderId>,
|
||||
pub entrypoint: CloudObjectEventEntrypoint,
|
||||
}
|
||||
|
||||
/// The data returned by the server when an object is created, generic to any object type.
|
||||
#[derive(Debug)]
|
||||
pub struct CreatedCloudObject {
|
||||
pub client_id: ClientId,
|
||||
pub revision_and_editor: RevisionAndLastEditor,
|
||||
pub metadata_ts: ServerTimestamp,
|
||||
pub server_id_and_type: ServerIdAndType,
|
||||
pub creator_uid: Option<String>,
|
||||
pub permissions: ServerPermissions,
|
||||
}
|
||||
|
||||
/// Result of attempting to create a cloud object.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug)]
|
||||
pub enum CreateCloudObjectResult {
|
||||
/// The object creation was successful.
|
||||
Success {
|
||||
created_cloud_object: CreatedCloudObject,
|
||||
},
|
||||
/// The object creation was denied due to an expected user error.
|
||||
UserFacingError(String),
|
||||
/// The object creation was rejected because the generic string object had
|
||||
/// already been created by another client.
|
||||
GenericStringObjectUniqueKeyConflict,
|
||||
}
|
||||
|
||||
/// Result of attempting to bulk create a cloud object.
|
||||
#[derive(Debug)]
|
||||
pub enum BulkCreateCloudObjectResult {
|
||||
/// The bulk object creation was successful.
|
||||
Success {
|
||||
created_cloud_objects: Vec<CreatedCloudObject>,
|
||||
},
|
||||
/// The bulk object creation was rejected because at least one generic string object had
|
||||
/// already been created by another client.
|
||||
GenericStringObjectUniqueKeyConflict,
|
||||
}
|
||||
|
||||
/// The creation-specific data returned by the server, which is inserted into CloudModel and persisted
|
||||
/// just once.
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct ServerCreationInfo {
|
||||
pub server_id_and_type: ServerIdAndType,
|
||||
pub creator_uid: Option<String>,
|
||||
pub permissions: ServerPermissions,
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{
|
||||
CloudObjectMetadata, CloudObjectPermissions, CloudObjectStatuses, CloudObjectSyncStatus,
|
||||
ConflictStatus, GenericServerObject, NumInFlightRequests, ObjectType, Owner,
|
||||
};
|
||||
use crate::ids::{ClientId, SyncId};
|
||||
|
||||
/// A portable payload for persisting or otherwise upserting a cloud object without app-local event types.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CloudObjectUpsertParams<M> {
|
||||
pub id: SyncId,
|
||||
pub object_type: ObjectType,
|
||||
pub metadata: CloudObjectMetadata,
|
||||
pub permissions: CloudObjectPermissions,
|
||||
pub model: M,
|
||||
}
|
||||
|
||||
/// A generic implementation of cloud objects that can be used for any model and id types.
|
||||
///
|
||||
/// For instance, rather than directly implementing the CloudObject trait, CloudObjects can
|
||||
/// implement GenericCloudObject<K, M> where K is their id type and M is their model type.
|
||||
///
|
||||
/// For example, CloudNotebook becomes:
|
||||
///
|
||||
/// pub type CloudNotebook = GenericCloudObject<NotebookId, CloudNotebookModel>
|
||||
///
|
||||
/// The advantage of using the generic model is you get common implementations
|
||||
/// of CloudObject methods like ```versions``` for free.
|
||||
///
|
||||
/// See the comments for CloudObject to understand the relationship between
|
||||
/// this trait, CloudObject and CloudModelType. They are tightly coupled.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GenericCloudObject<K, M> {
|
||||
pub id: SyncId,
|
||||
pub metadata: CloudObjectMetadata,
|
||||
pub permissions: CloudObjectPermissions,
|
||||
/// Tracks whether this object has a conflict with the server version.
|
||||
/// This is runtime state (not persisted) - conflicts are always NoConflicts when loaded from SQLite.
|
||||
pub conflict_status: ConflictStatus<GenericServerObject<K, M>>,
|
||||
|
||||
// Intentionally not public to prevent users of this class from holding
|
||||
// onto references to the model outside of this struct.
|
||||
//
|
||||
// This is an Arc in order to support clone-on-write semantics for the model.
|
||||
// By wrapping the model in an Arc, clones become cheap, and we can avoid
|
||||
// doing deep clones of the model whenever the containing object is cloned.
|
||||
//
|
||||
// Callers who want to update the model need to call set_model to update the
|
||||
// entire model atomically.
|
||||
model: Arc<M>,
|
||||
}
|
||||
|
||||
impl<K, M> PartialEq for GenericCloudObject<K, M>
|
||||
where
|
||||
M: PartialEq,
|
||||
{
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id && self.model() == other.model()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, M> GenericCloudObject<K, M> {
|
||||
/// Gets a reference to the model held by the object.
|
||||
pub fn model(&self) -> &M {
|
||||
&self.model
|
||||
}
|
||||
|
||||
/// Returns a shared handle to the model.
|
||||
pub fn shared_model(&self) -> Arc<M> {
|
||||
self.model.clone()
|
||||
}
|
||||
|
||||
/// Sets a new version of the model on the object, replacing the old version.
|
||||
pub fn set_model(&mut self, model: M) {
|
||||
self.model = model.into();
|
||||
}
|
||||
|
||||
/// Constructs a new instance of this model with the given id, model, metadata and permissions.
|
||||
pub fn new(
|
||||
id: SyncId,
|
||||
model: M,
|
||||
metadata: CloudObjectMetadata,
|
||||
permissions: CloudObjectPermissions,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
model: model.into(),
|
||||
metadata,
|
||||
permissions,
|
||||
conflict_status: ConflictStatus::NoConflicts,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new GenericCloudObject with the given model, owner, and initial folder id.
|
||||
/// This is for the local creation flow, as opposed to creating from a server update.
|
||||
pub fn new_local(
|
||||
model: M,
|
||||
owner: Owner,
|
||||
initial_folder_id: Option<SyncId>,
|
||||
client_id: ClientId,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: SyncId::ClientId(client_id),
|
||||
model: model.into(),
|
||||
metadata: CloudObjectMetadata {
|
||||
pending_changes_statuses: CloudObjectStatuses {
|
||||
content_sync_status: CloudObjectSyncStatus::InFlight(NumInFlightRequests(1)),
|
||||
has_pending_metadata_change: false,
|
||||
has_pending_permissions_change: false,
|
||||
pending_untrash: false,
|
||||
pending_delete: false,
|
||||
},
|
||||
folder_id: initial_folder_id,
|
||||
revision: Default::default(),
|
||||
metadata_last_updated_ts: Default::default(),
|
||||
current_editor_uid: Default::default(),
|
||||
trashed_ts: Default::default(),
|
||||
// Objects created from the client are never welcome objects.
|
||||
is_welcome_object: false,
|
||||
creator_uid: None,
|
||||
last_editor_uid: None,
|
||||
last_task_run_ts: None,
|
||||
},
|
||||
permissions: CloudObjectPermissions {
|
||||
owner,
|
||||
anyone_with_link: None,
|
||||
guests: Default::default(),
|
||||
permissions_last_updated_ts: None,
|
||||
},
|
||||
conflict_status: ConflictStatus::NoConflicts,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`GenericCloudObject`] from a [`GenericServerObject`].
|
||||
pub fn new_from_server(server_object: GenericServerObject<K, M>) -> Self {
|
||||
Self {
|
||||
id: server_object.id,
|
||||
model: server_object.model.into(),
|
||||
metadata: CloudObjectMetadata::new_from_server(server_object.metadata),
|
||||
permissions: CloudObjectPermissions::new_from_server(server_object.permissions),
|
||||
conflict_status: ConflictStatus::NoConflicts,
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks this object as being in conflict with the provided object.
|
||||
pub fn set_conflicting_object(&mut self, object: Arc<GenericServerObject<K, M>>) {
|
||||
self.conflict_status = ConflictStatus::ConflictingChanges { object };
|
||||
}
|
||||
|
||||
pub fn update_from_server_object(&mut self, server_object: GenericServerObject<K, M>) {
|
||||
// Check if we should create a conflict or apply the update.
|
||||
if self.metadata.has_pending_content_changes() || self.conflict_status.has_conflicts() {
|
||||
// There are pending changes, so this creates a conflict.
|
||||
self.conflict_status = ConflictStatus::ConflictingChanges {
|
||||
object: Arc::new(server_object),
|
||||
};
|
||||
} else {
|
||||
// No pending changes, apply the server update.
|
||||
self.metadata
|
||||
.update_revision_from_server(&server_object.metadata);
|
||||
self.model = server_object.model.into();
|
||||
self.conflict_status = ConflictStatus::NoConflicts;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns portable upsert parameters for this object.
|
||||
pub fn upsert_params(&self, object_type: ObjectType) -> CloudObjectUpsertParams<M>
|
||||
where
|
||||
M: Clone,
|
||||
{
|
||||
CloudObjectUpsertParams {
|
||||
id: self.id,
|
||||
object_type,
|
||||
metadata: self.metadata.clone(),
|
||||
permissions: self.permissions.clone(),
|
||||
model: self.model().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts this object into portable upsert parameters.
|
||||
pub fn into_upsert_params(self, object_type: ObjectType) -> CloudObjectUpsertParams<M>
|
||||
where
|
||||
M: Clone,
|
||||
{
|
||||
let Self {
|
||||
id,
|
||||
metadata,
|
||||
permissions,
|
||||
model,
|
||||
conflict_status: _,
|
||||
} = self;
|
||||
let model = Arc::try_unwrap(model).unwrap_or_else(|model| (*model).clone());
|
||||
CloudObjectUpsertParams {
|
||||
id,
|
||||
object_type,
|
||||
metadata,
|
||||
permissions,
|
||||
model,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, M> From<CloudObjectUpsertParams<M>> for GenericCloudObject<K, M> {
|
||||
fn from(params: CloudObjectUpsertParams<M>) -> Self {
|
||||
Self::new(params.id, params.model, params.metadata, params.permissions)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
use std::fmt::Debug;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use super::{GenericStringObjectFormat, ObjectType, SerializedModel, ServerObjectModel};
|
||||
|
||||
/// A serializer goes from a model to a string and back.
|
||||
pub trait Serializer<M>: Debug + Clone + 'static {
|
||||
fn model_format() -> GenericStringObjectFormat;
|
||||
fn serialize(model: &M) -> SerializedModel;
|
||||
fn deserialize_owned(serialized: &str) -> Result<M>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
/// A `GenericStringModel` is a generic implementation of model types that can serialize to/from string.
|
||||
/// given a particular serializer.
|
||||
#[derive(Clone, Debug, PartialEq, Default)]
|
||||
pub struct GenericStringModel<M, S>
|
||||
where
|
||||
S: Serializer<M>,
|
||||
{
|
||||
pub string_model: M,
|
||||
_serializer: PhantomData<fn() -> S>,
|
||||
}
|
||||
|
||||
impl<M, S> GenericStringModel<M, S>
|
||||
where
|
||||
S: Serializer<M>,
|
||||
{
|
||||
pub fn deserialize_owned(serialized: &str) -> Result<Self> {
|
||||
S::deserialize_owned(serialized).map(Self::new)
|
||||
}
|
||||
|
||||
pub fn new(model: M) -> Self {
|
||||
Self {
|
||||
string_model: model,
|
||||
_serializer: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn json_model(&self) -> &M {
|
||||
&self.string_model
|
||||
}
|
||||
}
|
||||
|
||||
impl<M, S> ServerObjectModel for GenericStringModel<M, S>
|
||||
where
|
||||
M: Debug + Clone + Send + Sync + 'static,
|
||||
S: Serializer<M>,
|
||||
{
|
||||
fn object_type(&self) -> ObjectType {
|
||||
ObjectType::GenericStringObject(S::model_format())
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
use std::any::Any;
|
||||
use std::fmt::{self, Debug};
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{ObjectType, ServerMetadata, ServerPermissions};
|
||||
use crate::ids::SyncId;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub enum ConflictStatus<T> {
|
||||
#[default]
|
||||
NoConflicts,
|
||||
ConflictingChanges {
|
||||
object: Arc<T>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<T> ConflictStatus<T> {
|
||||
/// Utility function that allows for a more ergonomic way of figuring out whether there is a
|
||||
/// conflict (for cases where we don't care about the conflict details).
|
||||
pub fn has_conflicts(&self) -> bool {
|
||||
matches!(self, ConflictStatus::ConflictingChanges { .. })
|
||||
}
|
||||
}
|
||||
|
||||
/// Common behavior that server-backed models expose to generic server objects.
|
||||
pub trait ServerObjectModel: Debug + Clone + Send + Sync + 'static {
|
||||
/// Returns the object type for this model.
|
||||
fn object_type(&self) -> ObjectType;
|
||||
}
|
||||
|
||||
/// Common trait for server objects that allows us to use them as trait objects
|
||||
/// and downcast to concrete types when needed.
|
||||
pub trait ServerObject: Debug + Send + Sync {
|
||||
/// Returns the object type of this server object
|
||||
fn object_type(&self) -> ObjectType;
|
||||
|
||||
/// Returns this object as a ref to the Any type. Needed for typecasts.
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
|
||||
/// Returns the trait object as a concrete type reference by downcasting it.
|
||||
/// Returns None if the downcast fails.
|
||||
fn as_concrete_type<K, M>(
|
||||
server_object: &dyn ServerObject,
|
||||
) -> Option<&GenericServerObject<K, M>>
|
||||
where
|
||||
Self: Sized,
|
||||
K: 'static,
|
||||
M: 'static,
|
||||
{
|
||||
server_object
|
||||
.as_any()
|
||||
.downcast_ref::<GenericServerObject<K, M>>()
|
||||
}
|
||||
|
||||
/// Returns a cloned boxed version of this server object.
|
||||
/// Note that we can't force the ServerObject trait to derive from Cloned
|
||||
/// directly because that would make the trait not object safe. This
|
||||
/// is a workaround.
|
||||
fn clone_box(&self) -> Box<dyn ServerObject>;
|
||||
}
|
||||
|
||||
/// An object that maps directly to the data returned from the server
|
||||
/// for a given model and id type.
|
||||
pub struct GenericServerObject<K, M> {
|
||||
pub id: SyncId,
|
||||
pub model: M,
|
||||
pub metadata: ServerMetadata,
|
||||
pub permissions: ServerPermissions,
|
||||
_marker: PhantomData<fn() -> K>,
|
||||
}
|
||||
|
||||
impl<K, M> Clone for GenericServerObject<K, M>
|
||||
where
|
||||
M: Clone,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self::new(
|
||||
self.id,
|
||||
self.model.clone(),
|
||||
self.metadata.clone(),
|
||||
self.permissions.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, M> Debug for GenericServerObject<K, M>
|
||||
where
|
||||
M: Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("GenericServerObject")
|
||||
.field("id", &self.id)
|
||||
.field("model", &self.model)
|
||||
.field("metadata", &self.metadata)
|
||||
.field("permissions", &self.permissions)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, M> GenericServerObject<K, M> {
|
||||
/// Constructs a server object from its server-provided parts.
|
||||
pub fn new(
|
||||
id: SyncId,
|
||||
model: M,
|
||||
metadata: ServerMetadata,
|
||||
permissions: ServerPermissions,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
model,
|
||||
metadata,
|
||||
permissions,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, K, M> From<&'a dyn ServerObject> for Option<&'a GenericServerObject<K, M>>
|
||||
where
|
||||
K: 'static,
|
||||
M: 'static,
|
||||
{
|
||||
fn from(value: &'a dyn ServerObject) -> Self {
|
||||
value.as_any().downcast_ref::<GenericServerObject<K, M>>()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, K, M> From<&'a Box<dyn ServerObject>> for Option<&'a GenericServerObject<K, M>>
|
||||
where
|
||||
K: 'static,
|
||||
M: 'static,
|
||||
{
|
||||
fn from(value: &'a Box<dyn ServerObject>) -> Self {
|
||||
value.as_ref().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, M> ServerObject for GenericServerObject<K, M>
|
||||
where
|
||||
K: 'static,
|
||||
M: ServerObjectModel,
|
||||
{
|
||||
fn object_type(&self) -> ObjectType {
|
||||
self.model.object_type()
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn clone_box(&self) -> Box<dyn ServerObject> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
use warp_graphql::queries::get_updated_cloud_objects::UpdatedObjectInput;
|
||||
|
||||
use super::RevisionAndLastEditor;
|
||||
|
||||
/// Result of attempting to update a cloud object.
|
||||
#[derive(Debug)]
|
||||
pub enum UpdateCloudObjectResult<T> {
|
||||
/// The update was successful and the object now has the specified revision.
|
||||
Success {
|
||||
revision_and_editor: RevisionAndLastEditor,
|
||||
},
|
||||
/// The update was rejected because the update was not sent from the current revision in
|
||||
/// storage. The object and revision in storage are returned.
|
||||
Rejected { object: T },
|
||||
}
|
||||
|
||||
/// Helper struct that contains all the info needed to fetch changed objects from the server.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct ObjectsToUpdate {
|
||||
pub notebooks: Vec<UpdatedObjectInput>,
|
||||
pub workflows: Vec<UpdatedObjectInput>,
|
||||
pub folders: Vec<UpdatedObjectInput>,
|
||||
pub generic_string_objects: Vec<UpdatedObjectInput>,
|
||||
}
|
||||
Reference in New Issue
Block a user