first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+2
View File
@@ -0,0 +1,2 @@
pub use user_uid::{TEST_USER_EMAIL, TEST_USER_UID, UserUid};
pub use warp_server_auth::user_uid;
@@ -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>,
}
+137
View File
@@ -0,0 +1,137 @@
pub mod sharing;
use crate::cloud_object::{GenericStringObjectFormat, ObjectIdType, ObjectType};
use crate::ids::{HashedSqliteId, ObjectUid, ServerId, SyncId};
/// Enum to use to pass down type and id between actions to avoid multiplying actions whenever we
/// need to pass the object id, etc.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum CloudObjectTypeAndId {
Notebook(SyncId),
Workflow(SyncId),
Folder(SyncId),
GenericStringObject {
object_type: GenericStringObjectFormat,
id: SyncId,
},
}
impl CloudObjectTypeAndId {
pub fn from_id_and_type(id: SyncId, object_type: ObjectType) -> Self {
match object_type {
ObjectType::Notebook => Self::Notebook(id),
ObjectType::Workflow => Self::Workflow(id),
ObjectType::Folder => Self::Folder(id),
ObjectType::GenericStringObject(format) => Self::GenericStringObject {
object_type: format,
id,
},
}
}
pub fn uid(self) -> ObjectUid {
match self {
Self::Notebook(id) => id.uid(),
Self::Workflow(id) => id.uid(),
Self::Folder(id) => id.uid(),
Self::GenericStringObject { id, .. } => id.uid(),
}
}
pub fn sync_id(self) -> SyncId {
match self {
Self::Notebook(id)
| Self::Workflow(id)
| Self::Folder(id)
| Self::GenericStringObject { id, .. } => id,
}
}
pub fn sqlite_uid_hash(self) -> HashedSqliteId {
match self {
CloudObjectTypeAndId::Notebook(id) => id.sqlite_uid_hash(ObjectIdType::Notebook),
CloudObjectTypeAndId::Workflow(id) => id.sqlite_uid_hash(ObjectIdType::Workflow),
CloudObjectTypeAndId::Folder(id) => id.sqlite_uid_hash(ObjectIdType::Folder),
CloudObjectTypeAndId::GenericStringObject { object_type: _, id } => {
id.sqlite_uid_hash(ObjectIdType::GenericStringObject)
}
}
}
pub fn object_id_type(&self) -> ObjectIdType {
match self {
CloudObjectTypeAndId::Notebook(_) => ObjectIdType::Notebook,
CloudObjectTypeAndId::Workflow(_) => ObjectIdType::Workflow,
CloudObjectTypeAndId::GenericStringObject { .. } => ObjectIdType::GenericStringObject,
CloudObjectTypeAndId::Folder(_) => ObjectIdType::Folder,
}
}
pub fn object_type(&self) -> ObjectType {
match self {
CloudObjectTypeAndId::Notebook(_) => ObjectType::Notebook,
CloudObjectTypeAndId::Workflow(_) => ObjectType::Workflow,
CloudObjectTypeAndId::Folder(_) => ObjectType::Folder,
CloudObjectTypeAndId::GenericStringObject { object_type, .. } => {
ObjectType::GenericStringObject(*object_type)
}
}
}
pub fn as_folder_id(self) -> Option<SyncId> {
match self {
CloudObjectTypeAndId::Notebook(_) => None,
CloudObjectTypeAndId::Workflow(_) => None,
CloudObjectTypeAndId::GenericStringObject { .. } => None,
CloudObjectTypeAndId::Folder(f) => Some(f),
}
}
pub fn as_notebook_id(self) -> Option<SyncId> {
match self {
CloudObjectTypeAndId::Notebook(id) => Some(id),
_ => None,
}
}
pub fn as_generic_string_object_id(self) -> Option<SyncId> {
match self {
CloudObjectTypeAndId::GenericStringObject { object_type: _, id } => Some(id),
_ => None,
}
}
pub fn has_server_id(self) -> bool {
matches!(
self,
CloudObjectTypeAndId::Notebook(SyncId::ServerId(_))
| CloudObjectTypeAndId::Workflow(SyncId::ServerId(_))
| CloudObjectTypeAndId::Folder(SyncId::ServerId(_))
| CloudObjectTypeAndId::GenericStringObject {
id: SyncId::ServerId(_),
..
}
)
}
pub fn server_id(self) -> Option<ServerId> {
match self {
CloudObjectTypeAndId::Notebook(SyncId::ServerId(notebook_id)) => Some(notebook_id),
CloudObjectTypeAndId::Workflow(SyncId::ServerId(workflow_id)) => Some(workflow_id),
CloudObjectTypeAndId::Folder(SyncId::ServerId(folder_id)) => Some(folder_id),
CloudObjectTypeAndId::GenericStringObject {
id: SyncId::ServerId(json_object_id),
..
} => Some(json_object_id),
_ => None,
}
}
pub fn drive_row_position_id(self) -> String {
format!("WarpDriveRow_{}", self.uid())
}
pub fn from_generic_string_object(object_type: GenericStringObjectFormat, id: SyncId) -> Self {
Self::GenericStringObject { object_type, id }
}
}
+223
View File
@@ -0,0 +1,223 @@
use std::str::FromStr;
use galaxy_graphql::object_permissions::AccessLevel;
use serde::{Deserialize, Serialize};
use session_sharing_protocol::common::{ProfileData as SessionSharingProfileData, Role};
use crate::auth::UserUid;
use crate::cloud_object::Owner;
use crate::ids::ServerId;
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum SharingAccessLevel {
View,
Edit,
Full,
}
impl SharingAccessLevel {
pub fn label(&self) -> &'static str {
match self {
SharingAccessLevel::View => "Can view",
SharingAccessLevel::Edit => "Can edit",
SharingAccessLevel::Full => "Full access",
}
}
pub fn name(&self) -> &'static str {
match self {
SharingAccessLevel::View => "view",
SharingAccessLevel::Edit => "edit",
SharingAccessLevel::Full => "access",
}
}
/// Whether or not this access level implies the `Trash` action.
pub fn can_trash(self) -> bool {
self >= SharingAccessLevel::Edit
}
/// Whether or not this access level implies the `DeletePermanently` action.
pub fn can_delete(self) -> bool {
self >= SharingAccessLevel::Full
}
/// Whether or not this access level implies the `ChangeOwner` action.
pub fn can_move_drive(self) -> bool {
self >= SharingAccessLevel::Full
}
/// Whether or not this access level implies the `EditAccess` action.
pub fn can_edit_access(self) -> bool {
self >= SharingAccessLevel::Full
}
/// Convert this access level to a serializable value, which can be parsed by [`FromStr`].
pub fn to_serializable_value(self) -> &'static str {
match self {
SharingAccessLevel::View => "VIEW",
SharingAccessLevel::Edit => "EDIT",
SharingAccessLevel::Full => "FULL",
}
}
}
impl FromStr for SharingAccessLevel {
type Err = anyhow::Error;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"VIEW" => Ok(Self::View),
"EDIT" => Ok(Self::Edit),
"FULL" => Ok(Self::Full),
_ => Err(anyhow::anyhow!("unknown access level {value}")),
}
}
}
impl From<AccessLevel> for SharingAccessLevel {
fn from(server_access: AccessLevel) -> Self {
match server_access {
AccessLevel::Viewer => Self::View,
AccessLevel::Editor => Self::Edit,
AccessLevel::Full => Self::Full,
}
}
}
impl From<SharingAccessLevel> for AccessLevel {
fn from(val: SharingAccessLevel) -> Self {
match val {
SharingAccessLevel::View => AccessLevel::Viewer,
SharingAccessLevel::Edit => AccessLevel::Editor,
SharingAccessLevel::Full => AccessLevel::Full,
}
}
}
impl From<Role> for SharingAccessLevel {
fn from(role: Role) -> Self {
match role {
Role::Reader => Self::View,
Role::Executor => Self::Edit,
Role::Full => Self::Full,
}
}
}
impl From<SharingAccessLevel> for Role {
fn from(access_level: SharingAccessLevel) -> Self {
match access_level {
SharingAccessLevel::View => Self::Reader,
SharingAccessLevel::Edit | SharingAccessLevel::Full => Self::Executor,
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum LinkSharingSubjectType {
None,
Anyone,
}
/// A `Subject` is someone with access to a shared object, like its owner or a directly-added
/// guest.
#[derive(Debug, Clone, PartialEq)]
pub enum Subject {
User(UserKind),
#[allow(dead_code)]
PendingUser {
email: Option<String>,
},
Team(TeamKind),
AnyoneWithLink(LinkSharingSubjectType),
}
/// A kind of user. In all cases, there is an underlying Warp account, but it's represented
/// differently in certain cases.
#[derive(Debug, Clone)]
pub enum UserKind {
/// A Warp user account, tracked in the [`UserProfiles`] model.
Account(UserUid),
/// A session-sharing participant.
// TODO(CLD-2283): Remove this once we have Firebase UIDs for shared session participants.
SharedSessionParticipant(SessionSharingProfileData),
}
/// A kind of team. Team permission updates are propagated differently for
/// shared sessions, so we need to store different info in certain cases.
#[derive(Debug, Clone, PartialEq)]
pub enum TeamKind {
Team {
team_uid: ServerId,
},
/// The team of the shared session sharer.
SharedSessionTeam {
team_uid: ServerId,
name: String,
},
}
impl TeamKind {
/// Gets the team UID.
pub fn team_uid(&self) -> ServerId {
match self {
TeamKind::Team { team_uid } => *team_uid,
TeamKind::SharedSessionTeam { team_uid, .. } => *team_uid,
}
}
}
impl Subject {
/// Convert an [`Owner`] into the closest [`Subject`] type.
pub fn from_owner(owner: Owner) -> Self {
match owner {
Owner::User { user_uid } => Subject::User(UserKind::Account(user_uid)),
Owner::Team { team_uid } => Subject::Team(TeamKind::Team { team_uid }),
}
}
/// Gets the user UID for this subject, if it has one.
pub fn user_uid(&self) -> Option<UserUid> {
match self {
Subject::User(user_kind) => match user_kind {
UserKind::Account(user_uid) => Some(*user_uid),
UserKind::SharedSessionParticipant(profile_data) => {
Some(UserUid::new(profile_data.firebase_uid.as_str()))
}
},
Subject::PendingUser { .. } => None,
Subject::Team(_) => None,
Subject::AnyoneWithLink(_) => None,
}
}
/// Checks if this subject refers to a given Firebase user directly.
pub fn is_user(&self, other_uid: UserUid) -> bool {
match self {
Subject::User(UserKind::Account(user_uid)) => *user_uid == other_uid,
Subject::User(UserKind::SharedSessionParticipant(profile_data)) => {
profile_data.firebase_uid.as_str() == other_uid.as_str()
}
_ => false,
}
}
/// Gets the team UID for this subject, if it has one.
pub fn team_uid(&self) -> Option<ServerId> {
match self {
Subject::Team(team_kind) => Some(team_kind.team_uid()),
_ => None,
}
}
}
impl PartialEq for UserKind {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Account(self_uid), Self::Account(other_uid)) => self_uid == other_uid,
// Shared session participant data does not implement `PartialEq`. We only compare
// `UserKind`s in tests, so support isn't yet needed.
_ => false,
}
}
}
+432
View File
@@ -0,0 +1,432 @@
use std::fmt;
use itertools::Itertools;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use uuid::Uuid;
use crate::cloud_object::ObjectIdType;
/// Convert ID enums into and from a hashed UUID.
pub trait HashableId: Sized + Send + Sync {
fn to_hash(&self) -> String;
fn from_hash(hash: &str) -> Option<Self>;
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize, schemars::JsonSchema)]
#[schemars(description = "A client-generated unique identifier.")]
pub struct ClientId(Uuid);
impl HashableId for ClientId {
fn to_hash(&self) -> String {
self.to_string()
}
fn from_hash(hash: &str) -> Option<ClientId> {
hash.strip_prefix("Client-")
.and_then(|s| Uuid::parse_str(s).ok())
.map(ClientId)
}
}
impl ClientId {
pub fn new() -> ClientId {
Self(Uuid::new_v4())
}
pub fn sqlite_hash(&self) -> String {
self.to_string()
}
}
impl Default for ClientId {
fn default() -> Self {
ClientId::new()
}
}
impl fmt::Display for ClientId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Client-{}", self.0)
}
}
impl From<String> for ClientId {
fn from(s: String) -> Self {
ClientId::from_hash(&s).unwrap_or_default()
}
}
/// ID of an object in the sync queue.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, schemars::JsonSchema)]
#[schemars(description = "Identifier for a synced object, either local or server-assigned.")]
pub enum SyncId {
/// Item has not been sync-ed yet. Using a client-created UUID.
#[schemars(
description = "A locally-generated identifier for an object not yet synced to the server."
)]
ClientId(ClientId),
/// Item has been sync-ed to the cloud. Using the server ID.
#[schemars(description = "A server-assigned identifier for a synced object.")]
ServerId(ServerId),
}
impl SyncId {
pub fn from_object_id<K>(id: K) -> Self
where
K: ToServerId,
{
Self::ServerId(id.to_server_id())
}
pub fn uid(&self) -> ObjectUid {
match self {
Self::ClientId(id) => id.to_string(),
Self::ServerId(id) => id.uid(),
}
}
pub fn sqlite_uid_hash(&self, object_id_type: ObjectIdType) -> String {
match self {
SyncId::ClientId(id) => id.sqlite_hash(),
SyncId::ServerId(id) => id.sqlite_type_and_uid_hash(object_id_type),
}
}
/// If this item has been synced to the cloud, extract its server ID.
pub fn into_server(self) -> Option<ServerId> {
match self {
Self::ServerId(id) => Some(id),
Self::ClientId(_) => None,
}
}
pub fn into_client(self) -> Option<ClientId> {
match self {
Self::ServerId(_) => None,
Self::ClientId(id) => Some(id),
}
}
}
impl settings_value::SettingsValue for SyncId {}
impl fmt::Display for SyncId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::ServerId(id) => id.fmt(f),
Self::ClientId(id) => id.fmt(f),
}
}
}
impl From<ServerId> for SyncId {
fn from(id: ServerId) -> SyncId {
SyncId::ServerId(id)
}
}
/// Custom serialize function for SyncIds.
impl Serialize for SyncId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
SyncId::ServerId(server_id) => server_id.serialize(serializer),
SyncId::ClientId(client_id) => client_id.to_hash().serialize(serializer),
}
}
}
/// Custom deserialize function for SyncIds.
impl<'de> Deserialize<'de> for SyncId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s: String = Deserialize::deserialize(deserializer)?;
// We try to deserialize as a ClientID, which only succeeds if the ID is prefixed with `Client-`.
// If that fails, we assume this is a server id and create a server ID.
if let Some(hashed) = ClientId::from_hash(s.as_str()) {
Ok(SyncId::ClientId(hashed))
} else {
Ok(SyncId::ServerId(ServerId::from_string_lossy(s)))
}
}
}
/// Length of the ServerId, should be in sync with the length picked for the server.
const SERVER_ID_LENGTH: usize = 22;
/// ServerId is a representation of a string-based unique ID we generate on the server,
/// of length SERVER_ID_LENGTH.
/// Because it's of fixed length, it can implement the Copy trait
/// (in contrast to simply using a String type).
#[derive(Clone, Copy, Default, Hash, PartialEq, Eq, schemars::JsonSchema)]
#[schemars(description = "A server-assigned unique identifier.")]
pub struct ServerId([char; SERVER_ID_LENGTH]);
/// For server IDs, this is the value that is stored
/// in the database. For client IDs, it is of the form "Client-{id}".
/// Used to index into cloud model and in most object read, write, and metadata
/// mutation server APIs.
pub type ObjectUid = String;
/// Corresponds to what is stored for a given object id within the local sqlite
/// database. Needed for backwards compatibility of the sqlite db following a refactor
/// that stripped the object type away from SyncID.
///
/// Of the format {sqlite_prefix}-{uid}.
///
/// Other than sqlite model events, this id is used for embedded objects within notebooks.
pub type HashedSqliteId = String;
/// UID for API keys.
pub type ApiKeyUid = String;
#[derive(Debug, thiserror::Error)]
pub enum ParseServerIdError {
#[error("ServerId must be exactly {SERVER_ID_LENGTH} characters, got {len}")]
InvalidLength { len: usize },
}
/// Removes the prefix from sqlite IDs to extract the UIDs. Should not be used unless there
/// is not other way to cleanly do the conversion, i.e., when we don't know the ID type.
#[allow(clippy::result_unit_err)]
pub fn parse_sqlite_id_to_uid(hashed_sqlite_id: HashedSqliteId) -> Result<ObjectUid, ()> {
let Some(uid) = hashed_sqlite_id.split("-").last() else {
return Err(());
};
Ok(uid.to_owned())
}
impl ServerId {
/// Convert a string input to a server ID. If the string is not exactly
/// [`SERVER_ID_LENGTH`] characters long, it will be truncated or padded as
/// necessary.
pub fn from_string_lossy(id: impl AsRef<str>) -> Self {
let id = id.as_ref();
Self::try_from(id).unwrap_or_else(|err| {
if cfg!(debug_assertions) {
panic!("{err}");
}
// ServerIds need to be exactly 22 characters, so to prevent a crash, we'll normalize
// the string. Nothing that uses it will work, but it's better than crashing.
let normalized = Self::normalize_id_str(id, 0);
Self::try_from(normalized).expect("id should convert")
})
}
/// Normalizes a string to be exactly 22 characters long.
fn normalize_id_str(input: &str, prefix_length: usize) -> String {
let available_len = SERVER_ID_LENGTH - prefix_length;
let truncated = if input.len() > available_len {
&input[input.len() - available_len..]
} else {
input
};
format!("{truncated:0>available_len$}")
}
pub fn uid(&self) -> ObjectUid {
(*self).into()
}
/// We need this API for backwards compatibility with local sqlite data.
/// In sqlite, objects are stored in object typy, uid pairs of the format
/// {sqlite-prefix}-{uid}. For example, for a workflow this would be
/// "Workflow-{uid}".
pub fn sqlite_type_and_uid_hash(&self, object_id_type: ObjectIdType) -> HashedSqliteId {
format!("{}-{}", object_id_type.sqlite_prefix(), self)
}
}
impl TryFrom<&str> for ServerId {
type Error = ParseServerIdError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s.chars().collect_array() {
Some(chars) => Ok(Self(chars)),
None => Err(ParseServerIdError::InvalidLength {
len: s.chars().count(),
}),
}
}
}
impl TryFrom<String> for ServerId {
type Error = ParseServerIdError;
fn try_from(id: String) -> Result<Self, Self::Error> {
Self::try_from(id.as_str())
}
}
/// Creates a conversion between an i64 and a corresponding deterministic ServerId for use in tests.
/// An i64 like 123 will be converted to "test_uid00000000000123".
#[cfg(any(test, feature = "test-util"))]
impl From<i64> for ServerId {
fn from(id: i64) -> Self {
let prefix = "test_uid";
let id_str = id.abs().to_string();
let normalized = format!(
"{}{}",
prefix,
Self::normalize_id_str(&id_str, prefix.len())
);
Self::try_from(normalized).expect("normalized string should always be valid")
}
}
impl From<ServerId> for String {
fn from(id: ServerId) -> String {
String::from_iter(id.0)
}
}
/// We need our own implementation of serialize, due to ServerId being essentially a char array.
/// The default serializer in this case would spit a string that looks like an array, instead of a
/// nicely formatted string that we want. This implementation would serialize ServerId('a', 'b') to
/// "ab" instead.
impl Serialize for ServerId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let s: String = (*self).into();
serializer.serialize_str(&s)
}
}
impl<'de> Deserialize<'de> for ServerId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s: String = Deserialize::deserialize(deserializer)?;
ServerId::try_from(s.as_str()).map_err(serde::de::Error::custom)
}
}
impl std::fmt::Display for ServerId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
use std::fmt::Write;
for ch in self.0.iter() {
f.write_char(*ch)?;
}
Ok(())
}
}
impl std::fmt::Debug for ServerId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(f, "ServerId({self})")
}
}
pub trait ToServerId {
fn to_server_id(&self) -> ServerId;
}
#[derive(Clone, Debug, PartialEq)]
pub struct ServerIdAndType {
pub id: ServerId,
pub id_type: ObjectIdType,
}
impl ServerIdAndType {
pub fn sqlite_type_and_uid_hash(&self) -> HashedSqliteId {
self.id.sqlite_type_and_uid_hash(self.id_type)
}
}
/// string_id_traits is a macro used for generating implementations for the type aliases on
/// ServerId, implements different To/From and Display, and HashableId traits.
/// Takes type and desired prefix for HashableId.
#[macro_export]
macro_rules! server_id_traits {
($t:ty, $prefix:literal) => {
#[cfg(any(test, feature = "test-util"))]
impl From<i64> for $t {
fn from(id: i64) -> Self {
Self(id.into())
}
}
impl From<String> for $t {
fn from(id: String) -> Self {
Self($crate::ids::ServerId::from_string_lossy(id))
}
}
impl From<$t> for String {
fn from(id: $t) -> String {
id.0.into()
}
}
impl std::fmt::Display for $t {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(f, "{}", self.0)
}
}
impl From<$t> for $crate::ids::ServerId {
fn from(id: $t) -> Self {
id.0
}
}
impl $crate::ids::HashableId for $t {
fn to_hash(&self) -> String {
format!("{}-{}", $prefix, self)
}
fn from_hash(hash: &str) -> Option<$t> {
hash.strip_prefix(&format!("{}-", $prefix))
.map(|s| s.to_string().into())
}
}
impl From<$crate::ids::ServerId> for $t {
fn from(id: $crate::ids::ServerId) -> Self {
Self(id)
}
}
impl $crate::ids::ToServerId for $t {
fn to_server_id(&self) -> $crate::ids::ServerId {
self.0
}
}
};
}
#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash, Default)]
pub struct FolderId(ServerId);
server_id_traits! { FolderId, "Folder" }
impl From<FolderId> for SyncId {
fn from(id: FolderId) -> Self {
Self::ServerId(id.into())
}
}
/// Object ID type that is common for all generic string objects.
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub struct GenericStringObjectId(ServerId);
crate::server_id_traits! { GenericStringObjectId, "GenericStringObject" }
impl From<GenericStringObjectId> for SyncId {
fn from(id: GenericStringObjectId) -> Self {
Self::ServerId(id.into())
}
}
impl GenericStringObjectId {
pub fn uid(&self) -> ObjectUid {
self.0.uid()
}
}
+14
View File
@@ -0,0 +1,14 @@
//! This crate defines the low-level, model-agnostic cloud object substrate shared by Warp crates.
//!
//! It owns server-facing identifiers, user identifiers, object metadata, object type and format
//! definitions, and sharing or drive primitives that do not depend on concrete Warp object models.
//!
//! It should remain independent of model-specific payloads, SQLite persistence, app runtime state,
//! and UI rendering concerns.
pub mod auth;
pub mod cloud_object;
pub mod drive;
pub mod ids;
pub use auth::UserUid;