diff --git a/.sqlx/query-0389af6b225125d09c5a75b443561dba4d97b786d040e5b8d5a76de36716beb2.json b/.sqlx/query-0389af6b225125d09c5a75b443561dba4d97b786d040e5b8d5a76de36716beb2.json new file mode 100644 index 000000000..c7b42f525 --- /dev/null +++ b/.sqlx/query-0389af6b225125d09c5a75b443561dba4d97b786d040e5b8d5a76de36716beb2.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT oid\n FROM af_collab\n WHERE workspace_id = $1\n AND partition_key = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "oid", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Int4" + ] + }, + "nullable": [ + false + ] + }, + "hash": "0389af6b225125d09c5a75b443561dba4d97b786d040e5b8d5a76de36716beb2" +} diff --git a/.sqlx/query-7af4023278eea11cf5db92040bd8947bd14df72a93eded96658187d3f9dc0e81.json b/.sqlx/query-4fffbac63c56402626b50f8f50e3ddbb1b566b6c064d8d397a4e073433a86c11.json similarity index 59% rename from .sqlx/query-7af4023278eea11cf5db92040bd8947bd14df72a93eded96658187d3f9dc0e81.json rename to .sqlx/query-4fffbac63c56402626b50f8f50e3ddbb1b566b6c064d8d397a4e073433a86c11.json index 40fe91565..e719ed770 100644 --- a/.sqlx/query-7af4023278eea11cf5db92040bd8947bd14df72a93eded96658187d3f9dc0e81.json +++ b/.sqlx/query-4fffbac63c56402626b50f8f50e3ddbb1b566b6c064d8d397a4e073433a86c11.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT EXISTS (SELECT 1 FROM af_collab_member WHERE oid = $1 AND uid = $2 LIMIT 1)\n ", + "query": "\n SELECT EXISTS (SELECT 1 FROM af_collab_member WHERE oid = $1 AND uid = $2 LIMIT 1)\n ", "describe": { "columns": [ { @@ -19,5 +19,5 @@ null ] }, - "hash": "7af4023278eea11cf5db92040bd8947bd14df72a93eded96658187d3f9dc0e81" + "hash": "4fffbac63c56402626b50f8f50e3ddbb1b566b6c064d8d397a4e073433a86c11" } diff --git a/.sqlx/query-7aa6e41c80f0b2906d46e73ae05e8e70e133b7edd450b102715b8a487d6055ac.json b/.sqlx/query-7aa6e41c80f0b2906d46e73ae05e8e70e133b7edd450b102715b8a487d6055ac.json new file mode 100644 index 000000000..01467eb67 --- /dev/null +++ b/.sqlx/query-7aa6e41c80f0b2906d46e73ae05e8e70e133b7edd450b102715b8a487d6055ac.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT metadata, blob\n FROM af_published_collab\n WHERE view_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "metadata", + "type_info": "Jsonb" + }, + { + "ordinal": 1, + "name": "blob", + "type_info": "Bytea" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "7aa6e41c80f0b2906d46e73ae05e8e70e133b7edd450b102715b8a487d6055ac" +} diff --git a/.sqlx/query-bc5df5a1fe64ed4f32654f09d0d62459d02f494912fb38b97f87c46b62a69b1f.json b/.sqlx/query-bc5df5a1fe64ed4f32654f09d0d62459d02f494912fb38b97f87c46b62a69b1f.json new file mode 100644 index 000000000..8dd1fb8c9 --- /dev/null +++ b/.sqlx/query-bc5df5a1fe64ed4f32654f09d0d62459d02f494912fb38b97f87c46b62a69b1f.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n aw.publish_namespace AS namespace,\n apc.publish_name,\n apc.view_id\n FROM af_published_collab apc\n LEFT JOIN af_workspace aw\n ON apc.workspace_id = aw.workspace_id\n WHERE apc.view_id = $1;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "namespace", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "publish_name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "view_id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "bc5df5a1fe64ed4f32654f09d0d62459d02f494912fb38b97f87c46b62a69b1f" +} diff --git a/.sqlx/query-1c93a309d53f3c5fc976716fcbb7c84abe5dad39806e54ac9270ec8d6f7eac8d.json b/.sqlx/query-d388782f755f0b164ef36c168af142baeb9bbd3cc2b8b7cd736b346580be8790.json similarity index 60% rename from .sqlx/query-1c93a309d53f3c5fc976716fcbb7c84abe5dad39806e54ac9270ec8d6f7eac8d.json rename to .sqlx/query-d388782f755f0b164ef36c168af142baeb9bbd3cc2b8b7cd736b346580be8790.json index 459ee5750..94ebdb5b1 100644 --- a/.sqlx/query-1c93a309d53f3c5fc976716fcbb7c84abe5dad39806e54ac9270ec8d6f7eac8d.json +++ b/.sqlx/query-d388782f755f0b164ef36c168af142baeb9bbd3cc2b8b7cd736b346580be8790.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT EXISTS (SELECT 1 FROM af_collab WHERE oid = $1 LIMIT 1)\n ", + "query": "\n SELECT EXISTS (SELECT 1 FROM af_collab WHERE oid = $1 LIMIT 1)\n ", "describe": { "columns": [ { @@ -18,5 +18,5 @@ null ] }, - "hash": "1c93a309d53f3c5fc976716fcbb7c84abe5dad39806e54ac9270ec8d6f7eac8d" + "hash": "d388782f755f0b164ef36c168af142baeb9bbd3cc2b8b7cd736b346580be8790" } diff --git a/.sqlx/query-9eba47895808e9968529d789a02758807486054028990fffef62fb89ac047750.json b/.sqlx/query-f1b56cf92eeb5f7ddda80876c1ecf5b6a5357a58d18b9bf6f14e6a2261bd1182.json similarity index 62% rename from .sqlx/query-9eba47895808e9968529d789a02758807486054028990fffef62fb89ac047750.json rename to .sqlx/query-f1b56cf92eeb5f7ddda80876c1ecf5b6a5357a58d18b9bf6f14e6a2261bd1182.json index 0f51b573f..bf8a77ad8 100644 --- a/.sqlx/query-9eba47895808e9968529d789a02758807486054028990fffef62fb89ac047750.json +++ b/.sqlx/query-f1b56cf92eeb5f7ddda80876c1ecf5b6a5357a58d18b9bf6f14e6a2261bd1182.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n uid, oid, access_level\n FROM af_collab_member\n INNER JOIN af_permissions\n ON af_collab_member.permission_id = af_permissions.id\n ", + "query": "\n SELECT uid, oid, access_level\n FROM af_collab_member\n INNER JOIN af_permissions\n ON af_collab_member.permission_id = af_permissions.id\n ", "describe": { "columns": [ { @@ -28,5 +28,5 @@ false ] }, - "hash": "9eba47895808e9968529d789a02758807486054028990fffef62fb89ac047750" + "hash": "f1b56cf92eeb5f7ddda80876c1ecf5b6a5357a58d18b9bf6f14e6a2261bd1182" } diff --git a/Cargo.lock b/Cargo.lock index 884dceab5..192bcb172 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -632,6 +632,7 @@ dependencies = [ "gotrue-entity", "governor", "handlebars", + "hex", "image", "infra", "itertools 0.11.0", @@ -678,6 +679,7 @@ dependencies = [ "validator", "workspace-access", "workspace-template", + "yrs", ] [[package]] @@ -2013,6 +2015,7 @@ dependencies = [ "client-api", "client-websocket", "collab", + "collab-database", "collab-document", "collab-entity", "collab-folder", @@ -2738,6 +2741,7 @@ dependencies = [ "chrono", "collab", "collab-entity", + "collab-rt-entity", "database-entity", "futures-util", "pgvector", @@ -6295,6 +6299,7 @@ dependencies = [ "serde_json", "serde_repr", "thiserror", + "tracing", "uuid", "validator", ] diff --git a/Cargo.toml b/Cargo.toml index 0cf69ce61..1230f96e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -139,6 +139,7 @@ shared-entity = { path = "libs/shared-entity", features = ["cloud"] } workspace-template = { workspace = true } collab-rt-entity.workspace = true collab-stream.workspace = true +yrs.workspace = true tonic-build = "0.11.0" log = "0.4.20" @@ -164,6 +165,7 @@ client-api = { path = "libs/client-api", features = [ opener = "0.6.1" image = "0.23.14" collab-rt-entity.workspace = true +hex = "0.4.3" [[bin]] name = "appflowy_cloud" diff --git a/Dockerfile b/Dockerfile index c2edccce3..a38e15804 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Using cargo-chef to manage Rust build cache effectively -FROM lukemathwalker/cargo-chef:latest-rust-1.77 as chef +FROM lukemathwalker/cargo-chef:latest-rust-1.79 as chef WORKDIR /app RUN apt update && apt install lld clang -y diff --git a/libs/client-api-test/Cargo.toml b/libs/client-api-test/Cargo.toml index 00f70e2d1..ae7499c1c 100644 --- a/libs/client-api-test/Cargo.toml +++ b/libs/client-api-test/Cargo.toml @@ -26,6 +26,7 @@ image = "0.23.14" database-entity.workspace = true collab-entity.workspace = true shared-entity.workspace = true +collab-database.workspace = true tracing-subscriber = { version = "0.3.18", features = ["registry", "env-filter", "ansi", "json"] } uuid = "1.6.1" lazy_static = "1.4.0" @@ -43,4 +44,4 @@ web-sys = { version = "0.3", features = ["console"] } [features] collab-sync = ["client-api/collab-sync"] -ai-test-enabled = [] \ No newline at end of file +ai-test-enabled = [] diff --git a/libs/client-api-test/src/test_client.rs b/libs/client-api-test/src/test_client.rs index 2a4b674cb..ad8d688b9 100644 --- a/libs/client-api-test/src/test_client.rs +++ b/libs/client-api-test/src/test_client.rs @@ -1,7 +1,5 @@ use std::borrow::BorrowMut; use std::collections::HashMap; -use std::ops::Deref; -use std::path::{Path, PathBuf}; use std::sync::Arc; use anyhow::{anyhow, Error}; @@ -185,13 +183,38 @@ impl TestClient { Folder::from_collab_doc_state( uid, CollabOrigin::Empty, - DataSource::DocStateV1(data.encode_collab.doc_state.to_vec()), + data.encode_collab.into(), &workspace_id, vec![], ) .unwrap() } + pub async fn get_workspace_database_collab(&mut self, workspace_id: &str) -> Collab { + let db_storage_id = self.open_workspace(workspace_id).await.database_storage_id; + let ws_db_doc_state = self + .get_collab(QueryCollabParams { + workspace_id: workspace_id.to_string(), + inner: QueryCollab { + object_id: db_storage_id.to_string(), + collab_type: CollabType::WorkspaceDatabase, + }, + }) + .await + .unwrap() + .encode_collab + .doc_state + .to_vec(); + Collab::new_with_source( + CollabOrigin::Server, + &db_storage_id.to_string(), + DataSource::DocStateV1(ws_db_doc_state), + vec![], + false, + ) + .unwrap() + } + pub async fn get_user_awareness(&self) -> UserAwareness { let workspace_id = self.workspace_id().await; let profile = self.get_user_profile().await; @@ -969,31 +992,3 @@ pub async fn get_collab_json_from_server( .unwrap() .to_json_value() } - -pub struct TestTempFile(PathBuf); - -impl TestTempFile { - fn cleanup(dir: &PathBuf) { - let _ = std::fs::remove_dir_all(dir); - } -} - -impl AsRef for TestTempFile { - fn as_ref(&self) -> &Path { - &self.0 - } -} - -impl Deref for TestTempFile { - type Target = PathBuf; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Drop for TestTempFile { - fn drop(&mut self) { - Self::cleanup(&self.0) - } -} diff --git a/libs/client-api/src/http.rs b/libs/client-api/src/http.rs index 18314a6ea..1c936873e 100644 --- a/libs/client-api/src/http.rs +++ b/libs/client-api/src/http.rs @@ -1,5 +1,7 @@ use crate::notify::{ClientToken, TokenStateReceiver}; use app_error::AppError; +use client_api_entity::workspace_dto::FolderView; +use client_api_entity::workspace_dto::QueryWorkspaceFolder; use client_api_entity::workspace_dto::QueryWorkspaceParam; use client_api_entity::AuthProvider; use client_api_entity::CollabType; @@ -657,6 +659,37 @@ impl Client { .into_data() } + /// List out the views in the workspace recursively. + /// The depth parameter specifies the depth of the folder view tree to return(default: 1). + /// e.g., depth=1 will return only up to `Shared` and `PrivateSpace` + /// depth=2 will return up to `mydoc1`, `mydoc2`, `mydoc3`, `mydoc4` + /// + /// . MyWorkspace + /// ├── Shared + /// │ ├── mydoc1 + /// │ └── mydoc2 + /// └── PrivateSpace + /// ├── mydoc3 + /// └── mydoc4 + #[instrument(level = "info", skip_all, err)] + pub async fn get_workspace_folder( + &self, + workspace_id: &str, + depth: Option, + ) -> Result { + let url = format!("{}/api/workspace/{}/folder", self.base_url, workspace_id); + let resp = self + .http_client_with_auth(Method::GET, &url) + .await? + .query(&QueryWorkspaceFolder { depth }) + .send() + .await?; + log_request_id(&resp); + AppResponse::::from_response(resp) + .await? + .into_data() + } + #[instrument(level = "info", skip_all, err)] pub async fn open_workspace(&self, workspace_id: &str) -> Result { let url = format!("{}/api/workspace/{}/open", self.base_url, workspace_id); diff --git a/libs/client-api/src/http_publish.rs b/libs/client-api/src/http_publish.rs index 334a5e4fe..80b4530df 100644 --- a/libs/client-api/src/http_publish.rs +++ b/libs/client-api/src/http_publish.rs @@ -1,7 +1,8 @@ use bytes::Bytes; +use client_api_entity::{workspace_dto::PublishedDuplicate, PublishInfo, UpdatePublishNamespace}; use client_api_entity::{ CreateGlobalCommentParams, CreateReactionParams, DeleteGlobalCommentParams, DeleteReactionParams, - GetReactionQueryParams, GlobalComments, PublishInfo, Reactions, UpdatePublishNamespace, + GetReactionQueryParams, GlobalComments, Reactions, }; use reqwest::Method; use shared_entity::response::{AppResponse, AppResponseError}; @@ -259,6 +260,24 @@ impl Client { Ok(bytes) } + pub async fn duplicate_published_to_workspace( + &self, + workspace_id: &str, + publish_duplicate: &PublishedDuplicate, + ) -> Result<(), AppResponseError> { + let url = format!( + "{}/api/workspace/{}/published-duplicate", + self.base_url, workspace_id + ); + let resp = self + .http_client_with_auth(Method::POST, &url) + .await? + .json(publish_duplicate) + .send() + .await?; + AppResponse::<()>::from_response(resp).await?.into_error() + } + pub async fn get_published_view_reactions( &self, view_id: &uuid::Uuid, diff --git a/libs/database/Cargo.toml b/libs/database/Cargo.toml index d1ef7c1b2..b6afd8a2e 100644 --- a/libs/database/Cargo.toml +++ b/libs/database/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] collab = { workspace = true } collab-entity = { workspace = true } +collab-rt-entity = { workspace = true } validator = { version = "0.16", features = ["validator_derive", "derive"] } database-entity.workspace = true app-error = { workspace = true, features = ["sqlx_error", "validation_error"] } diff --git a/libs/database/src/collab/collab_db_ops.rs b/libs/database/src/collab/collab_db_ops.rs index 0baaba139..984391517 100644 --- a/libs/database/src/collab/collab_db_ops.rs +++ b/libs/database/src/collab/collab_db_ops.rs @@ -496,11 +496,10 @@ pub fn select_collab_member_access_level( sqlx::query_as!( AFCollabMemberAccessLevelRow, r#" - SELECT - uid, oid, access_level + SELECT uid, oid, access_level FROM af_collab_member INNER JOIN af_permissions - ON af_collab_member.permission_id = af_permissions.id + ON af_collab_member.permission_id = af_permissions.id "# ) .fetch(pg_pool) @@ -513,17 +512,17 @@ pub async fn select_collab_members( ) -> Result, AppError> { let members = sqlx::query( r#" - SELECT af_collab_member.uid, - af_collab_member.oid, - af_permissions.id, - af_permissions.name, - af_permissions.access_level, - af_permissions.description + SELECT af_collab_member.uid, + af_collab_member.oid, + af_permissions.id, + af_permissions.name, + af_permissions.access_level, + af_permissions.description FROM af_collab_member JOIN af_permissions ON af_collab_member.permission_id = af_permissions.id WHERE af_collab_member.oid = $1 ORDER BY af_collab_member.created_at ASC - "#, + "#, ) .bind(oid) .try_map(collab_member_try_from_row) @@ -541,11 +540,11 @@ pub async fn select_collab_member<'a, E: Executor<'a, Database = Postgres>>( ) -> Result { let row = sqlx::query( r#" - SELECT af_collab_member.uid, af_collab_member.oid, af_permissions.id, af_permissions.name, af_permissions.access_level, af_permissions.description - FROM af_collab_member - JOIN af_permissions ON af_collab_member.permission_id = af_permissions.id - WHERE af_collab_member.uid = $1 AND af_collab_member.oid = $2 - "#, + SELECT af_collab_member.uid, af_collab_member.oid, af_permissions.id, af_permissions.name, af_permissions.access_level, af_permissions.description + FROM af_collab_member + JOIN af_permissions ON af_collab_member.permission_id = af_permissions.id + WHERE af_collab_member.uid = $1 AND af_collab_member.oid = $2 + "#, ) .bind(uid) .bind(oid) @@ -580,8 +579,8 @@ pub async fn is_collab_member_exists<'a, E: Executor<'a, Database = Postgres>>( ) -> Result { let result = sqlx::query_scalar!( r#" - SELECT EXISTS (SELECT 1 FROM af_collab_member WHERE oid = $1 AND uid = $2 LIMIT 1) - "#, + SELECT EXISTS (SELECT 1 FROM af_collab_member WHERE oid = $1 AND uid = $2 LIMIT 1) + "#, &oid, &uid, ) @@ -616,11 +615,30 @@ pub async fn is_collab_exists<'a, E: Executor<'a, Database = Postgres>>( ) -> Result { let result = sqlx::query_scalar!( r#" - SELECT EXISTS (SELECT 1 FROM af_collab WHERE oid = $1 LIMIT 1) - "#, + SELECT EXISTS (SELECT 1 FROM af_collab WHERE oid = $1 LIMIT 1) + "#, &oid, ) .fetch_one(executor) .await; transform_record_not_found_error(result) } + +pub async fn select_workspace_database_oid<'a, E: Executor<'a, Database = Postgres>>( + executor: E, + workspace_id: &Uuid, +) -> Result { + let partition_key = partition_key_from_collab_type(&CollabType::WorkspaceDatabase); + sqlx::query_scalar!( + r#" + SELECT oid + FROM af_collab + WHERE workspace_id = $1 + AND partition_key = $2 + "#, + &workspace_id, + &partition_key, + ) + .fetch_one(executor) + .await +} diff --git a/libs/database/src/collab/collab_storage.rs b/libs/database/src/collab/collab_storage.rs index 5bf357f23..d64ef27ac 100644 --- a/libs/database/src/collab/collab_storage.rs +++ b/libs/database/src/collab/collab_storage.rs @@ -8,6 +8,7 @@ use database_entity::dto::{ use collab::entity::EncodedCollab; use collab_entity::CollabType; +use collab_rt_entity::ClientCollabMessage; use serde::{Deserialize, Serialize}; use sqlx::Transaction; use std::collections::HashMap; @@ -123,6 +124,16 @@ pub trait CollabStorage: Send + Sync + 'static { from_editing_collab: bool, ) -> AppResult; + /// Sends a collab message to all connected clients. + /// # Arguments + /// * `object_id` - The ID of the collaboration object. + /// * `collab_messages` - The list of collab messages to broadcast. + async fn broadcast_encode_collab( + &self, + object_id: String, + collab_messages: Vec, + ) -> Result<(), AppError>; + async fn batch_get_collab( &self, uid: &i64, @@ -223,6 +234,17 @@ where .await } + async fn broadcast_encode_collab( + &self, + object_id: String, + collab_messages: Vec, + ) -> Result<(), AppError> { + self + .as_ref() + .broadcast_encode_collab(object_id, collab_messages) + .await + } + async fn batch_get_collab( &self, uid: &i64, diff --git a/libs/database/src/lib.rs b/libs/database/src/lib.rs index 6b248ae14..0ffbe1e74 100644 --- a/libs/database/src/lib.rs +++ b/libs/database/src/lib.rs @@ -5,6 +5,7 @@ pub mod history; pub mod index; pub mod listener; pub mod pg_row; +pub mod publish; pub mod resource_usage; pub mod template; pub mod user; diff --git a/libs/database/src/publish.rs b/libs/database/src/publish.rs new file mode 100644 index 000000000..15597e077 --- /dev/null +++ b/libs/database/src/publish.rs @@ -0,0 +1,275 @@ +use app_error::AppError; +use database_entity::dto::{PublishCollabItem, PublishInfo}; +use sqlx::{Executor, PgPool, Postgres}; +use uuid::Uuid; + +pub async fn select_user_is_collab_publisher_for_all_views( + pg_pool: &PgPool, + user_uuid: &Uuid, + workspace_uuid: &Uuid, + view_ids: &[Uuid], +) -> Result { + let count = sqlx::query_scalar!( + r#" + SELECT COUNT(*) + FROM af_published_collab + WHERE workspace_id = $1 + AND view_id = ANY($2) + AND published_by = (SELECT uid FROM af_user WHERE uuid = $3) + "#, + workspace_uuid, + view_ids, + user_uuid, + ) + .fetch_one(pg_pool) + .await?; + + match count { + Some(c) => Ok(c == view_ids.len() as i64), + None => Ok(false), + } +} + +#[inline] +pub async fn select_workspace_publish_namespace_exists<'a, E: Executor<'a, Database = Postgres>>( + executor: E, + workspace_id: &Uuid, + namespace: &str, +) -> Result { + let res = sqlx::query_scalar!( + r#" + SELECT EXISTS( + SELECT 1 + FROM af_workspace + WHERE workspace_id = $1 + AND publish_namespace = $2 + ) + "#, + workspace_id, + namespace, + ) + .fetch_one(executor) + .await?; + + Ok(res.unwrap_or(false)) +} + +#[inline] +pub async fn update_workspace_publish_namespace<'a, E: Executor<'a, Database = Postgres>>( + executor: E, + workspace_id: &Uuid, + new_namespace: &str, +) -> Result<(), AppError> { + let res = sqlx::query!( + r#" + UPDATE af_workspace + SET publish_namespace = $1 + WHERE workspace_id = $2 + "#, + new_namespace, + workspace_id, + ) + .execute(executor) + .await?; + + if res.rows_affected() != 1 { + tracing::error!( + "Failed to update workspace publish namespace, workspace_id: {}, new_namespace: {}, rows_affected: {}", + workspace_id, new_namespace, res.rows_affected() + ); + } + + Ok(()) +} + +#[inline] +pub async fn select_workspace_publish_namespace<'a, E: Executor<'a, Database = Postgres>>( + executor: E, + workspace_id: &Uuid, +) -> Result { + let res = sqlx::query_scalar!( + r#" + SELECT publish_namespace + FROM af_workspace + WHERE workspace_id = $1 + "#, + workspace_id, + ) + .fetch_one(executor) + .await?; + + Ok(res) +} + +#[inline] +pub async fn insert_or_replace_publish_collabs<'a, E: Executor<'a, Database = Postgres>>( + executor: E, + workspace_id: &Uuid, + publisher_uuid: &Uuid, + publish_items: Vec>>, +) -> Result<(), AppError> { + let item_count = publish_items.len(); + let mut view_ids: Vec = Vec::with_capacity(item_count); + let mut publish_names: Vec = Vec::with_capacity(item_count); + let mut metadatas: Vec = Vec::with_capacity(item_count); + let mut blobs: Vec> = Vec::with_capacity(item_count); + publish_items.into_iter().for_each(|item| { + view_ids.push(item.meta.view_id); + publish_names.push(item.meta.publish_name); + metadatas.push(item.meta.metadata); + blobs.push(item.data); + }); + + let res = sqlx::query!( + r#" + INSERT INTO af_published_collab (workspace_id, view_id, publish_name, published_by, metadata, blob) + SELECT * FROM UNNEST( + (SELECT array_agg((SELECT $1::uuid)) FROM generate_series(1, $7))::uuid[], + $2::uuid[], + $3::text[], + (SELECT array_agg((SELECT uid FROM af_user WHERE uuid = $4)) FROM generate_series(1, $7))::bigint[], + $5::jsonb[], + $6::bytea[] + ) + ON CONFLICT (workspace_id, view_id) DO UPDATE + SET metadata = EXCLUDED.metadata, + blob = EXCLUDED.blob, + published_by = EXCLUDED.published_by, + publish_name = EXCLUDED.publish_name + "#, + workspace_id, + &view_ids, + &publish_names, + publisher_uuid, + &metadatas, + &blobs, + item_count as i32, + ) + .execute(executor) + .await?; + + if res.rows_affected() != item_count as u64 { + tracing::warn!( + "Failed to insert or replace publish collab meta batch, workspace_id: {}, publisher_uuid: {}, rows_affected: {}, item_count: {}", + workspace_id, publisher_uuid, res.rows_affected(), item_count + ); + } + + Ok(()) +} + +#[inline] +pub async fn select_publish_collab_meta<'a, E: Executor<'a, Database = Postgres>>( + executor: E, + publish_namespace: &str, + publish_name: &str, +) -> Result { + let res = sqlx::query!( + r#" + SELECT metadata + FROM af_published_collab + WHERE workspace_id = (SELECT workspace_id FROM af_workspace WHERE publish_namespace = $1) + AND publish_name = $2 + "#, + publish_namespace, + publish_name, + ) + .fetch_one(executor) + .await?; + let metadata: serde_json::Value = res.metadata; + Ok(metadata) +} + +#[inline] +pub async fn delete_published_collabs<'a, E: Executor<'a, Database = Postgres>>( + executor: E, + workspace_id: &Uuid, + view_ids: &[Uuid], +) -> Result<(), AppError> { + let res = sqlx::query!( + r#" + DELETE FROM af_published_collab + WHERE workspace_id = $1 + AND view_id = ANY($2) + "#, + workspace_id, + view_ids, + ) + .execute(executor) + .await?; + + if res.rows_affected() != view_ids.len() as u64 { + tracing::error!( + "Failed to delete published collabs, workspace_id: {}, view_ids: {:?}, rows_affected: {}", + workspace_id, + view_ids, + res.rows_affected() + ); + } + + Ok(()) +} + +#[inline] +pub async fn select_published_data_for_view_id( + pg_pool: &PgPool, + view_id: &Uuid, +) -> Result)>, AppError> { + let res = sqlx::query!( + r#" + SELECT metadata, blob + FROM af_published_collab + WHERE view_id = $1 + "#, + view_id, + ) + .fetch_optional(pg_pool) + .await?; + Ok(res.map(|res| (res.metadata, res.blob))) +} + +#[inline] +pub async fn select_published_collab_blob<'a, E: Executor<'a, Database = Postgres>>( + executor: E, + publish_namespace: &str, + publish_name: &str, +) -> Result, AppError> { + let res = sqlx::query_scalar!( + r#" + SELECT blob + FROM af_published_collab + WHERE workspace_id = (SELECT workspace_id FROM af_workspace WHERE publish_namespace = $1) + AND publish_name = $2 + "#, + publish_namespace, + publish_name, + ) + .fetch_one(executor) + .await?; + + Ok(res) +} + +pub async fn select_published_collab_info<'a, E: Executor<'a, Database = Postgres>>( + executor: E, + view_id: &Uuid, +) -> Result { + let res = sqlx::query_as!( + PublishInfo, + r#" + SELECT + aw.publish_namespace AS namespace, + apc.publish_name, + apc.view_id + FROM af_published_collab apc + LEFT JOIN af_workspace aw + ON apc.workspace_id = aw.workspace_id + WHERE apc.view_id = $1; + "#, + view_id, + ) + .fetch_one(executor) + .await?; + + Ok(res) +} diff --git a/libs/shared-entity/Cargo.toml b/libs/shared-entity/Cargo.toml index bbc4d8b17..fed5c7297 100644 --- a/libs/shared-entity/Cargo.toml +++ b/libs/shared-entity/Cargo.toml @@ -28,6 +28,7 @@ validator = { version = "0.16", features = ["validator_derive", "derive"], optio futures = "0.3.30" bytes = "1.6.0" log = "0.4.21" +tracing = { workspace = true } [features] diff --git a/libs/shared-entity/src/dto/mod.rs b/libs/shared-entity/src/dto/mod.rs index 34d98890d..5eef0c86e 100644 --- a/libs/shared-entity/src/dto/mod.rs +++ b/libs/shared-entity/src/dto/mod.rs @@ -2,5 +2,6 @@ pub mod ai_dto; pub mod auth_dto; pub mod billing_dto; pub mod history_dto; +pub mod publish_dto; pub mod search_dto; pub mod workspace_dto; diff --git a/libs/shared-entity/src/dto/publish_dto.rs b/libs/shared-entity/src/dto/publish_dto.rs new file mode 100644 index 000000000..9a9d5e6cc --- /dev/null +++ b/libs/shared-entity/src/dto/publish_dto.rs @@ -0,0 +1,61 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use super::workspace_dto::{ViewIcon, ViewLayout}; + +/// Copied from AppFlowy-IO/AppFlowy/frontend/rust-lib/flowy-folder-pub/src/entities.rs +/// TODO(zack): make AppFlowy use from this crate instead +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PublishViewMeta { + pub metadata: PublishViewMetaData, + pub view_id: String, + pub publish_name: String, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)] +pub struct PublishViewMetaData { + pub view: PublishViewInfo, + pub child_views: Vec, + pub ancestor_views: Vec, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)] +pub struct PublishViewInfo { + pub view_id: String, + pub name: String, + pub icon: Option, + pub layout: ViewLayout, + pub extra: Option, + pub created_by: Option, + pub last_edited_by: Option, + pub last_edited_time: i64, + pub created_at: i64, + pub child_views: Option>, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PublishDatabasePayload { + pub meta: PublishViewMeta, + pub data: PublishDatabaseData, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default, Eq, PartialEq)] +pub struct PublishDatabaseData { + /// The encoded collab data for the database itself + pub database_collab: Vec, + + /// The encoded collab data for the database rows + /// Use the row_id as the key + pub database_row_collabs: HashMap>, + + /// The encoded collab data for the documents inside the database rows + /// It's not used for now + pub database_row_document_collabs: HashMap>, + + /// Visible view ids + pub visible_database_view_ids: Vec, + + /// Relation view id map + pub database_relations: HashMap, +} diff --git a/libs/shared-entity/src/dto/workspace_dto.rs b/libs/shared-entity/src/dto/workspace_dto.rs index d8d214454..2ee1085f1 100644 --- a/libs/shared-entity/src/dto/workspace_dto.rs +++ b/libs/shared-entity/src/dto/workspace_dto.rs @@ -2,6 +2,7 @@ use chrono::{DateTime, Utc}; use collab_entity::{CollabType, EncodedCollab}; use database_entity::dto::{AFRole, AFWorkspaceInvitationStatus}; use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; use std::ops::Deref; use uuid::Uuid; @@ -121,7 +122,54 @@ pub struct CollabResponse { pub object_id: String, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PublishedDuplicate { + pub published_view_id: String, + pub dest_view_id: String, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct FolderView { + pub view_id: String, + pub name: String, + pub icon: Option, + pub is_space: bool, + pub is_private: bool, + /// contains fields like `is_space`, and font information + pub extra: Option, + pub children: Vec, +} + +#[derive(Eq, PartialEq, Debug, Hash, Clone, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum IconType { + Emoji = 0, + Url = 1, + Icon = 2, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub struct ViewIcon { + pub ty: IconType, + pub value: String, +} + +#[derive(Eq, PartialEq, Debug, Hash, Clone, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum ViewLayout { + Document = 0, + Grid = 1, + Board = 2, + Calendar = 3, + Chat = 4, +} + #[derive(Default, Debug, Deserialize, Serialize)] pub struct QueryWorkspaceParam { pub include_member_count: Option, } + +#[derive(Default, Debug, Deserialize, Serialize)] +pub struct QueryWorkspaceFolder { + pub depth: Option, +} diff --git a/script/client_api_deps_check.sh b/script/client_api_deps_check.sh index 324189550..36ea29266 100755 --- a/script/client_api_deps_check.sh +++ b/script/client_api_deps_check.sh @@ -3,7 +3,7 @@ # Generate the current dependency list cargo tree > current_deps.txt -BASELINE_COUNT=609 +BASELINE_COUNT=610 CURRENT_COUNT=$(cat current_deps.txt | wc -l) echo "Expected dependency count (baseline): $BASELINE_COUNT" diff --git a/services/appflowy-collaborate/src/collab/mem_cache.rs b/services/appflowy-collaborate/src/collab/mem_cache.rs index 22bbb41f4..dee20268c 100644 --- a/services/appflowy-collaborate/src/collab/mem_cache.rs +++ b/services/appflowy-collaborate/src/collab/mem_cache.rs @@ -203,7 +203,7 @@ impl CollabMemCache { // Perform update only if the new timestamp is greater than the existing one if current_value .as_ref() - .map_or(true, |(ts, _)| timestamp > *ts) + .map_or(true, |(ts, _)| timestamp >= *ts) { let mut pipeline = pipe(); let data = [timestamp.to_be_bytes().as_ref(), data].concat(); diff --git a/services/appflowy-collaborate/src/collab/storage.rs b/services/appflowy-collaborate/src/collab/storage.rs index 618ac23ae..3c534d60b 100644 --- a/services/appflowy-collaborate/src/collab/storage.rs +++ b/services/appflowy-collaborate/src/collab/storage.rs @@ -5,10 +5,11 @@ use std::time::Duration; use async_trait::async_trait; use collab::entity::EncodedCollab; use collab_entity::CollabType; +use collab_rt_entity::ClientCollabMessage; use itertools::{Either, Itertools}; use sqlx::Transaction; -use tokio::sync::oneshot; use tokio::time::timeout; +use tracing::warn; use tracing::{error, instrument, trace}; use validator::Validate; @@ -123,7 +124,7 @@ where async fn get_encode_collab_from_editing(&self, object_id: &str) -> Option { let object_id = object_id.to_string(); - let (ret, rx) = oneshot::channel(); + let (ret, rx) = tokio::sync::oneshot::channel(); let timeout_duration = Duration::from_secs(5); // Attempt to send the command to the realtime server @@ -410,4 +411,39 @@ where error!("Failed to remove connected user: {}", err); } } + + async fn broadcast_encode_collab( + &self, + object_id: String, + collab_messages: Vec, + ) -> Result<(), AppError> { + let (sender, recv) = tokio::sync::oneshot::channel(); + + self + .rt_cmd_sender + .send(CollaborationCommand::ServerSendCollabMessage { + object_id, + collab_messages, + ret: sender, + }) + .await + .map_err(|err| { + AppError::Unhandled(format!( + "Failed to send encode collab command to realtime server: {}", + err + )) + })?; + + match recv.await { + Ok(res) => + if let Err(err) = res { + error!("Failed to broadcast encode collab: {}", err); + } + , + // caller may have dropped the receiver + Err(err) => warn!("Failed to receive response from realtime server: {}", err), + } + + Ok(()) + } } diff --git a/services/appflowy-collaborate/src/command.rs b/services/appflowy-collaborate/src/command.rs index 40761915a..4478f606c 100644 --- a/services/appflowy-collaborate/src/command.rs +++ b/services/appflowy-collaborate/src/command.rs @@ -1,5 +1,9 @@ -use crate::group::cmd::{GroupCommand, GroupCommandSender}; +use crate::{ + error::RealtimeError, + group::cmd::{GroupCommand, GroupCommandSender}, +}; use collab::entity::EncodedCollab; +use collab_rt_entity::ClientCollabMessage; use dashmap::DashMap; use std::sync::Arc; use tracing::error; @@ -13,6 +17,11 @@ pub enum CollaborationCommand { object_id: String, ret: EncodeCollabSender, }, + ServerSendCollabMessage { + object_id: String, + collab_messages: Vec, + ret: tokio::sync::oneshot::Sender>, + }, } pub(crate) fn spawn_collaboration_command( @@ -41,6 +50,24 @@ pub(crate) fn spawn_collaboration_command( }, } }, + CollaborationCommand::ServerSendCollabMessage { + object_id, + collab_messages, + ret, + } => { + if let Some(sender) = group_sender_by_object_id.get(&object_id) { + if let Err(err) = sender + .send(GroupCommand::HandleServerCollabMessage { + object_id, + collab_messages, + ret, + }) + .await + { + tracing::error!("Send group command error: {}", err); + }; + } + }, } } }); diff --git a/services/appflowy-collaborate/src/group/broadcast.rs b/services/appflowy-collaborate/src/group/broadcast.rs index d2cf37f38..4eda7943d 100644 --- a/services/appflowy-collaborate/src/group/broadcast.rs +++ b/services/appflowy-collaborate/src/group/broadcast.rs @@ -218,7 +218,10 @@ impl CollabBroadcast { error!("fail to broadcast message:{}", err); } } - Err(_) => break, + Err(e) => { + error!("fail to receive message:{}", e); + break; + }, } }, } @@ -313,7 +316,7 @@ async fn handle_client_messages( Ok(response) => { trace!("[realtime]: sending response: {}", response); match sink.send(response.into()).await { - Ok(_) => {}, + Ok(()) => {}, Err(err) => { trace!("[realtime]: send failed: {}", err); break; diff --git a/services/appflowy-collaborate/src/group/cmd.rs b/services/appflowy-collaborate/src/group/cmd.rs index 5c2752a75..2dee91e45 100644 --- a/services/appflowy-collaborate/src/group/cmd.rs +++ b/services/appflowy-collaborate/src/group/cmd.rs @@ -1,6 +1,8 @@ +use std::collections::HashMap; use std::sync::Arc; use async_stream::stream; +use collab::core::origin::CollabOrigin; use collab::entity::EncodedCollab; use dashmap::DashMap; use futures_util::StreamExt; @@ -19,6 +21,7 @@ use crate::group::manager::GroupManager; /// Using [GroupCommand] to interact with the group /// - HandleClientCollabMessage: Handle the client message /// - EncodeCollab: Encode the collab +/// - HandleServerCollabMessage: Handle the server message pub enum GroupCommand { HandleClientCollabMessage { user: RealtimeUser, @@ -30,6 +33,11 @@ pub enum GroupCommand { object_id: String, ret: tokio::sync::oneshot::Sender>, }, + HandleServerCollabMessage { + object_id: String, + collab_messages: Vec, + ret: tokio::sync::oneshot::Sender>, + }, } pub type GroupCommandSender = tokio::sync::mpsc::Sender; @@ -88,6 +96,18 @@ where warn!("Send encode collab fail"); } }, + GroupCommand::HandleServerCollabMessage { + object_id, + collab_messages, + ret, + } => { + let res = self + .handle_server_collab_messages(object_id, collab_messages) + .await; + if let Err(err) = ret.send(res) { + warn!("Send handle server collab message result fail: {:?}", err); + } + }, } }) .await; @@ -169,6 +189,50 @@ where Ok(()) } + /// similar to `handle_client_collab_message`, but the messages are sent from the server instead. + #[instrument(level = "trace", skip_all)] + async fn handle_server_collab_messages( + &self, + object_id: String, + messages: Vec, + ) -> Result<(), RealtimeError> { + if messages.is_empty() { + warn!("Unexpected empty collab messages sent from server"); + return Ok(()); + } + + let server_rt_user = RealtimeUser { + uid: 0, + device_id: "server".to_string(), + connect_at: chrono::Utc::now().timestamp_millis(), + session_id: uuid::Uuid::new_v4().to_string(), + app_version: "".to_string(), + }; + + if let Some(group) = self.group_manager.get_group(&object_id).await { + let (collab_message_sender, _collab_message_receiver) = futures::channel::mpsc::channel(1); + let (mut message_by_oid_sender, message_by_oid_receiver) = futures::channel::mpsc::channel(1); + group + .subscribe( + &server_rt_user, + CollabOrigin::Server, + collab_message_sender, + message_by_oid_receiver, + ) + .await; + let message = HashMap::from([(object_id.clone(), messages)]); + if let Err(err) = message_by_oid_sender.try_send(message) { + tracing::error!( + "failed to send message to group: {}, object_id: {}", + err, + object_id + ); + } + }; + + Ok(()) + } + async fn subscribe_group( &self, user: &RealtimeUser, diff --git a/services/appflowy-collaborate/src/group/group_init.rs b/services/appflowy-collaborate/src/group/group_init.rs index ecdff2816..09c8b5564 100644 --- a/services/appflowy-collaborate/src/group/group_init.rs +++ b/services/appflowy-collaborate/src/group/group_init.rs @@ -164,6 +164,7 @@ impl CollabGroup { ); if let Some(mut old) = self.subscribers.insert((*user).clone(), sub) { + tracing::warn!("{}: remove old subscriber: {}", &self.object_id, user); old.stop().await; } diff --git a/src/api/workspace.rs b/src/api/workspace.rs index 5b94b8296..1aa5144ec 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -26,6 +26,8 @@ use collab_rt_entity::RealtimeMessage; use collab_rt_protocol::validate_encode_collab; use database::collab::{CollabStorage, GetCollabOrigin}; use database::user::select_uid_from_email; +use database_entity::dto::PublishCollabItem; +use database_entity::dto::PublishInfo; use database_entity::dto::*; use shared_entity::dto::workspace_dto::*; use shared_entity::response::AppResponseError; @@ -143,6 +145,10 @@ pub fn workspace_scope() -> Scope { web::resource("/published/{publish_namespace}/{publish_name}/blob") .route(web::get().to(get_published_collab_blob_handler)) ) + .service( + web::resource("{workspace_id}/published-duplicate") + .route(web::post().to(post_published_duplicate_handler)) + ) .service( web::resource("/published-info/{view_id}") .route(web::get().to(get_published_collab_info_handler)) @@ -169,6 +175,10 @@ pub fn workspace_scope() -> Scope { .route(web::post().to(post_publish_collabs_handler)) .route(web::delete().to(delete_published_collabs_handler)) ) + .service( + web::resource("/{workspace_id}/folder") + .route(web::get().to(get_workspace_folder_handler)) + ) .service( web::resource("/{workspace_id}/collab/{object_id}/member/list") .route(web::get().to(get_collab_member_list_handler)), @@ -1049,7 +1059,7 @@ async fn put_publish_namespace_handler( ) -> Result>> { let workspace_id = workspace_id.into_inner(); let new_namespace = payload.into_inner().new_namespace; - biz::workspace::ops::set_workspace_namespace( + biz::workspace::publish::set_workspace_namespace( &state.pg_pool, &user_uuid, &workspace_id, @@ -1065,7 +1075,7 @@ async fn get_publish_namespace_handler( ) -> Result>> { let workspace_id = workspace_id.into_inner(); let namespace = - biz::workspace::ops::get_workspace_publish_namespace(&state.pg_pool, &workspace_id).await?; + biz::workspace::publish::get_workspace_publish_namespace(&state.pg_pool, &workspace_id).await?; Ok(Json(AppResponse::Ok().with_data(namespace))) } @@ -1074,9 +1084,12 @@ async fn get_published_collab_handler( state: Data, ) -> Result> { let (workspace_namespace, publish_name) = path_param.into_inner(); - let metadata = - biz::workspace::ops::get_published_collab(&state.pg_pool, &workspace_namespace, &publish_name) - .await?; + let metadata = biz::workspace::publish::get_published_collab( + &state.pg_pool, + &workspace_namespace, + &publish_name, + ) + .await?; Ok(Json(metadata)) } @@ -1085,7 +1098,7 @@ async fn get_published_collab_blob_handler( state: Data, ) -> Result> { let (publish_namespace, publish_name) = path_param.into_inner(); - let collab_data = biz::workspace::ops::get_published_collab_blob( + let collab_data = biz::workspace::publish::get_published_collab_blob( &state.pg_pool, &publish_namespace, &publish_name, @@ -1094,13 +1107,33 @@ async fn get_published_collab_blob_handler( Ok(collab_data) } +async fn post_published_duplicate_handler( + user_uuid: UserUuid, + workspace_id: web::Path, + state: Data, + params: Json, +) -> Result>> { + let uid = state.user_cache.get_user_uid(&user_uuid).await?; + let params = params.into_inner(); + biz::workspace::publish_dup::duplicate_published_collab_to_workspace( + &state.pg_pool, + state.collab_access_control_storage.clone(), + uid, + params.published_view_id, + workspace_id.into_inner(), + params.dest_view_id, + ) + .await?; + Ok(Json(AppResponse::Ok())) +} + async fn get_published_collab_info_handler( view_id: web::Path, state: Data, ) -> Result>> { let view_id = view_id.into_inner(); let collab_data = - biz::workspace::ops::get_published_collab_info(&state.pg_pool, &view_id).await?; + biz::workspace::publish::get_published_collab_info(&state.pg_pool, &view_id).await?; Ok(Json(AppResponse::Ok().with_data(collab_data))) } @@ -1205,7 +1238,7 @@ async fn post_publish_collabs_handler( let meta: PublishCollabMetadata = { let meta_len = payload_reader.read_u32_little_endian().await?; if meta_len > 4 * 1024 * 1024 { - // 4MB Limit for metadata + // 4MiB Limit for metadata return Err(AppError::InvalidRequest(String::from("metadata too large")).into()); } if meta_len == 0 { @@ -1219,8 +1252,8 @@ async fn post_publish_collabs_handler( let data = { let data_len = payload_reader.read_u32_little_endian().await?; - if data_len > 128 * 1024 * 1024 { - // 128MB Limit for data + if data_len > 32 * 1024 * 1024 { + // 32MiB Limit for data return Err(AppError::InvalidRequest(String::from("data too large")).into()); } let mut data_buffer = vec![0; data_len as usize]; @@ -1236,7 +1269,7 @@ async fn post_publish_collabs_handler( AppError::InvalidRequest(String::from("did not receive any data to publish")).into(), ); } - biz::workspace::ops::publish_collabs(&state.pg_pool, &workspace_id, &user_uuid, &accumulator) + biz::workspace::publish::publish_collabs(&state.pg_pool, &workspace_id, &user_uuid, accumulator) .await?; Ok(Json(AppResponse::Ok())) } @@ -1252,7 +1285,7 @@ async fn delete_published_collabs_handler( if view_ids.is_empty() { return Ok(Json(AppResponse::Ok())); } - biz::workspace::ops::delete_published_workspace_collab( + biz::workspace::publish::delete_published_workspace_collab( &state.pg_pool, &workspace_id, &view_ids, @@ -1340,6 +1373,24 @@ async fn get_workspace_usage_handler( Ok(Json(AppResponse::Ok().with_data(res))) } +async fn get_workspace_folder_handler( + user_uuid: UserUuid, + workspace_id: web::Path, + state: Data, + query: web::Query, +) -> Result>> { + let depth = query.depth.unwrap_or(1); + let uid = state.user_cache.get_user_uid(&user_uuid).await?; + let folder_view = biz::collab::ops::get_user_workspace_structure( + state.collab_access_control_storage.clone(), + uid, + workspace_id.into_inner(), + depth, + ) + .await?; + Ok(Json(AppResponse::Ok().with_data(folder_view))) +} + #[inline] async fn parser_realtime_msg( payload: Bytes, diff --git a/src/biz/collab/folder_view.rs b/src/biz/collab/folder_view.rs new file mode 100644 index 000000000..dab741d3b --- /dev/null +++ b/src/biz/collab/folder_view.rs @@ -0,0 +1,162 @@ +use std::collections::HashSet; + +use collab_folder::Folder; +use shared_entity::dto::workspace_dto::FolderView; +use uuid::Uuid; + +pub fn collab_folder_to_folder_view(folder: &Folder, depth: u32) -> FolderView { + let mut unviewable = HashSet::new(); + for private_section in folder.get_all_private_sections() { + unviewable.insert(private_section.id); + } + for trash_view in folder.get_all_trash_sections() { + unviewable.insert(trash_view.id); + } + + let mut private_views = HashSet::new(); + for private_section in folder.get_my_private_sections() { + unviewable.remove(&private_section.id); + private_views.insert(private_section.id); + } + + let workspace_id = folder.get_workspace_id().unwrap_or_else(|| { + tracing::error!("failed to get workspace_id"); + Uuid::nil().to_string() + }); + let root = match folder.get_view(&workspace_id) { + Some(root) => root, + None => { + tracing::error!("failed to get root view, workspace_id: {}", workspace_id); + return FolderView::default(); + }, + }; + + let extra = root.extra.as_deref().map(|extra| { + serde_json::from_str::(extra).unwrap_or_else(|e| { + tracing::error!("failed to parse extra field({}): {}", extra, e); + serde_json::Value::Null + }) + }); + + FolderView { + view_id: root.id.clone(), + name: root.name.clone(), + icon: root + .icon + .as_ref() + .map(|icon| to_dto_view_icon(icon.clone())), + is_space: false, + is_private: false, + extra, + children: if depth == 0 { + vec![] + } else { + root + .children + .iter() + .filter(|v| !unviewable.contains(&v.id)) + .map(|v| { + let intermediate = FolderViewIntermediate { + folder, + view_id: &v.id, + unviewable: &unviewable, + private_views: &private_views, + depth, + }; + FolderView::from(intermediate) + }) + .collect() + }, + } +} + +struct FolderViewIntermediate<'a> { + folder: &'a Folder, + view_id: &'a str, + unviewable: &'a HashSet, + private_views: &'a HashSet, + depth: u32, +} + +impl<'a> From> for FolderView { + fn from(fv: FolderViewIntermediate) -> Self { + let view = match fv.folder.get_view(fv.view_id) { + Some(view) => view, + None => { + tracing::error!("failed to get view, view_id: {}", fv.view_id); + return Self::default(); + }, + }; + let extra = view.extra.as_deref().map(|extra| { + serde_json::from_str::(extra).unwrap_or_else(|e| { + tracing::error!("failed to parse extra field({}): {}", extra, e); + serde_json::Value::Null + }) + }); + + Self { + view_id: view.id.clone(), + name: view.name.clone(), + icon: view + .icon + .as_ref() + .map(|icon| to_dto_view_icon(icon.clone())), + is_space: view_is_space(&view), + is_private: fv.private_views.contains(&view.id), + extra, + children: if fv.depth == 1 { + vec![] + } else { + view + .children + .iter() + .filter(|v| !fv.unviewable.contains(&v.id)) + .map(|v| { + FolderView::from(FolderViewIntermediate { + folder: fv.folder, + view_id: &v.id, + unviewable: fv.unviewable, + private_views: fv.private_views, + depth: fv.depth - 1, + }) + }) + .collect() + }, + } + } +} + +fn view_is_space(view: &collab_folder::View) -> bool { + let extra = match view.extra.as_ref() { + Some(extra) => extra, + None => return false, + }; + let value = match serde_json::from_str::(extra) { + Ok(v) => v, + Err(e) => { + tracing::error!("failed to parse extra field({}): {}", extra, e); + return false; + }, + }; + match value.get("is_space") { + Some(is_space_str) => is_space_str.as_bool().unwrap_or(false), + None => false, + } +} + +fn to_dto_view_icon(icon: collab_folder::ViewIcon) -> shared_entity::dto::workspace_dto::ViewIcon { + shared_entity::dto::workspace_dto::ViewIcon { + ty: to_dto_view_icon_type(icon.ty), + value: icon.value, + } +} + +fn to_dto_view_icon_type( + icon: collab_folder::IconType, +) -> shared_entity::dto::workspace_dto::IconType { + match icon { + collab_folder::IconType::Emoji => shared_entity::dto::workspace_dto::IconType::Emoji, + collab_folder::IconType::Url => shared_entity::dto::workspace_dto::IconType::Url, + collab_folder::IconType::Icon => shared_entity::dto::workspace_dto::IconType::Icon, + } +} diff --git a/src/biz/collab/mod.rs b/src/biz/collab/mod.rs index 109ec9d1c..1795e8b27 100644 --- a/src/biz/collab/mod.rs +++ b/src/biz/collab/mod.rs @@ -1,2 +1,3 @@ pub mod access_control; +pub mod folder_view; pub mod ops; diff --git a/src/biz/collab/ops.rs b/src/biz/collab/ops.rs index 7952b1f35..1520a0c9e 100644 --- a/src/biz/collab/ops.rs +++ b/src/biz/collab/ops.rs @@ -1,17 +1,30 @@ +use std::sync::Arc; + +use app_error::AppError; +use appflowy_collaborate::collab::storage::CollabAccessControlStorage; +use collab_entity::CollabType; +use collab_entity::EncodedCollab; +use collab_folder::{CollabOrigin, Folder}; +use database::collab::{CollabStorage, GetCollabOrigin}; +use database_entity::dto::QueryCollab; +use database_entity::dto::QueryCollabParams; +use sqlx::PgPool; use std::ops::DerefMut; use anyhow::Context; -use sqlx::{types::Uuid, PgPool}; +use shared_entity::dto::workspace_dto::FolderView; +use sqlx::types::Uuid; use tracing::{event, trace}; use validator::Validate; use access_control::collab::CollabAccessControl; -use app_error::AppError; use database_entity::dto::{ AFCollabMember, CollabMemberIdentify, InsertCollabMemberParams, QueryCollabMembers, UpdateCollabMemberParams, }; +use super::folder_view::collab_folder_to_folder_view; + /// Create a new collab member /// If the collab member already exists, return [AppError::RecordAlreadyExists] /// If the collab member does not exist, create a new one @@ -129,6 +142,7 @@ pub async fn delete_collab_member( .context("fail to commit the transaction to remove collab member")?; Ok(()) } + pub async fn get_collab_member_list( pg_pool: &PgPool, params: &QueryCollabMembers, @@ -137,3 +151,67 @@ pub async fn get_collab_member_list( let collab_member = database::collab::select_collab_members(¶ms.object_id, pg_pool).await?; Ok(collab_member) } + +pub async fn get_user_workspace_structure( + collab_storage: Arc, + uid: i64, + workspace_id: String, + depth: u32, +) -> Result { + let depth_limit = 10; + if depth > depth_limit { + return Err(AppError::InvalidRequest(format!( + "Depth {} is too large (limit: {})", + depth, depth_limit + ))); + } + let folder = get_latest_collab_folder(collab_storage, &uid, &workspace_id).await?; + let folder_view: FolderView = collab_folder_to_folder_view(&folder, depth); + Ok(folder_view) +} + +pub async fn get_latest_collab_folder( + collab_storage: Arc, + uid: &i64, + workspace_id: &str, +) -> Result { + let encoded_collab = get_latest_collab_encoded( + collab_storage, + uid, + workspace_id, + workspace_id, + CollabType::Folder, + ) + .await?; + let folder = Folder::from_collab_doc_state( + uid, + CollabOrigin::Server, + encoded_collab.into(), + workspace_id, + vec![], + ) + .map_err(|e| AppError::Unhandled(e.to_string()))?; + Ok(folder) +} + +pub async fn get_latest_collab_encoded( + collab_storage: Arc, + uid: &i64, + workspace_id: &str, + oid: &str, + collab_type: CollabType, +) -> Result { + collab_storage + .get_encode_collab( + GetCollabOrigin::User { uid: *uid }, + QueryCollabParams { + workspace_id: workspace_id.to_string(), + inner: QueryCollab { + object_id: oid.to_string(), + collab_type, + }, + }, + true, + ) + .await +} diff --git a/src/biz/workspace/mod.rs b/src/biz/workspace/mod.rs index 109ec9d1c..7c0935e97 100644 --- a/src/biz/workspace/mod.rs +++ b/src/biz/workspace/mod.rs @@ -1,2 +1,4 @@ pub mod access_control; pub mod ops; +pub mod publish; +pub mod publish_dup; diff --git a/src/biz/workspace/ops.rs b/src/biz/workspace/ops.rs index 052ba7688..50e64f886 100644 --- a/src/biz/workspace/ops.rs +++ b/src/biz/workspace/ops.rs @@ -1,8 +1,9 @@ use authentication::jwt::OptionalUserUuid; -use database_entity::dto::{AFWorkspaceSettingsChange, PublishCollabItem}; +use database_entity::dto::AFWorkspaceSettingsChange; +use database_entity::dto::PublishCollabItem; +use database_entity::dto::PublishInfo; use std::collections::HashMap; -use database_entity::dto::PublishInfo; use std::ops::DerefMut; use std::sync::Arc; @@ -639,7 +640,7 @@ pub async fn update_workspace_settings( Ok(setting) } -async fn check_workspace_owner( +pub async fn check_workspace_owner( pg_pool: &PgPool, user_uuid: &Uuid, workspace_id: &Uuid, diff --git a/src/biz/workspace/publish.rs b/src/biz/workspace/publish.rs new file mode 100644 index 000000000..941b4ead7 --- /dev/null +++ b/src/biz/workspace/publish.rs @@ -0,0 +1,156 @@ +use app_error::AppError; +use database_entity::dto::{PublishCollabItem, PublishInfo}; +use sqlx::PgPool; +use uuid::Uuid; + +use database::{ + publish::{ + delete_published_collabs, insert_or_replace_publish_collabs, select_publish_collab_meta, + select_published_collab_blob, select_published_collab_info, + select_user_is_collab_publisher_for_all_views, select_workspace_publish_namespace, + select_workspace_publish_namespace_exists, update_workspace_publish_namespace, + }, + workspace::select_user_is_workspace_owner, +}; + +use super::ops::check_workspace_owner; + +pub async fn publish_collabs( + pg_pool: &PgPool, + workspace_id: &Uuid, + publisher_uuid: &Uuid, + publish_items: Vec>>, +) -> Result<(), AppError> { + for publish_item in &publish_items { + check_collab_publish_name(publish_item.meta.publish_name.as_str())?; + } + insert_or_replace_publish_collabs(pg_pool, workspace_id, publisher_uuid, publish_items).await?; + Ok(()) +} + +pub async fn get_published_collab( + pg_pool: &PgPool, + publish_namespace: &str, + publish_name: &str, +) -> Result { + let metadata = select_publish_collab_meta(pg_pool, publish_namespace, publish_name).await?; + Ok(metadata) +} + +pub async fn get_published_collab_blob( + pg_pool: &PgPool, + publish_namespace: &str, + publish_name: &str, +) -> Result, AppError> { + select_published_collab_blob(pg_pool, publish_namespace, publish_name).await +} + +pub async fn get_published_collab_info( + pg_pool: &PgPool, + view_id: &Uuid, +) -> Result { + select_published_collab_info(pg_pool, view_id).await +} + +pub async fn delete_published_workspace_collab( + pg_pool: &PgPool, + workspace_id: &Uuid, + view_ids: &[Uuid], + user_uuid: &Uuid, +) -> Result<(), AppError> { + check_workspace_owner_or_publisher(pg_pool, user_uuid, workspace_id, view_ids).await?; + delete_published_collabs(pg_pool, workspace_id, view_ids).await?; + Ok(()) +} + +async fn check_workspace_owner_or_publisher( + pg_pool: &PgPool, + user_uuid: &Uuid, + workspace_id: &Uuid, + view_id: &[Uuid], +) -> Result<(), AppError> { + let is_owner = select_user_is_workspace_owner(pg_pool, user_uuid, workspace_id).await?; + if !is_owner { + let is_publisher = + select_user_is_collab_publisher_for_all_views(pg_pool, user_uuid, workspace_id, view_id) + .await?; + if !is_publisher { + return Err(AppError::UserUnAuthorized( + "User is not the owner of the workspace or the publisher of the document".to_string(), + )); + } + } + Ok(()) +} + +fn check_collab_publish_name(publish_name: &str) -> Result<(), AppError> { + // Check len + if publish_name.len() > 128 { + return Err(AppError::InvalidRequest( + "Publish name must be at most 128 characters long".to_string(), + )); + } + + // Only contain alphanumeric characters and hyphens + for c in publish_name.chars() { + if !c.is_alphanumeric() && c != '-' { + return Err(AppError::InvalidRequest( + "Publish name must only contain alphanumeric characters and hyphens".to_string(), + )); + } + } + + Ok(()) +} + +pub async fn set_workspace_namespace( + pg_pool: &PgPool, + user_uuid: &Uuid, + workspace_id: &Uuid, + new_namespace: &str, +) -> Result<(), AppError> { + check_workspace_owner(pg_pool, user_uuid, workspace_id).await?; + check_workspace_namespace(new_namespace).await?; + if select_workspace_publish_namespace_exists(pg_pool, workspace_id, new_namespace).await? { + return Err(AppError::PublishNamespaceAlreadyTaken( + "publish namespace is already taken".to_string(), + )); + }; + update_workspace_publish_namespace(pg_pool, workspace_id, new_namespace).await?; + Ok(()) +} + +pub async fn get_workspace_publish_namespace( + pg_pool: &PgPool, + workspace_id: &Uuid, +) -> Result { + select_workspace_publish_namespace(pg_pool, workspace_id).await +} + +async fn check_workspace_namespace(new_namespace: &str) -> Result<(), AppError> { + // Check len + if new_namespace.len() < 8 { + return Err(AppError::InvalidRequest( + "Namespace must be at least 8 characters long".to_string(), + )); + } + + if new_namespace.len() > 64 { + return Err(AppError::InvalidRequest( + "Namespace must be at most 32 characters long".to_string(), + )); + } + + // Only contain alphanumeric characters and hyphens + for c in new_namespace.chars() { + if !c.is_alphanumeric() && c != '-' { + return Err(AppError::InvalidRequest( + "Namespace must only contain alphanumeric characters and hyphens".to_string(), + )); + } + } + + // TODO: add more checks for reserved words + + Ok(()) +} diff --git a/src/biz/workspace/publish_dup.rs b/src/biz/workspace/publish_dup.rs new file mode 100644 index 000000000..db43c09e0 --- /dev/null +++ b/src/biz/workspace/publish_dup.rs @@ -0,0 +1,959 @@ +use app_error::AppError; +use appflowy_collaborate::collab::storage::CollabAccessControlStorage; +use collab::core::collab::DataSource; +use collab::preclude::Collab; + +use collab::preclude::MapExt; +use collab_database::views::ViewMap; +use collab_database::workspace_database::WorkspaceDatabaseBody; +use collab_document::blocks::DocumentData; +use collab_document::document::Document; +use collab_entity::CollabType; +use collab_folder::{CollabOrigin, Folder, RepeatedViewIdentifier, View}; +use collab_rt_entity::{ClientCollabMessage, UpdateSync}; +use collab_rt_protocol::{Message, SyncMessage}; +use database::collab::{select_workspace_database_oid, CollabStorage}; +use database::publish::select_published_data_for_view_id; +use database_entity::dto::CollabParams; +use shared_entity::dto::publish_dto::{PublishDatabaseData, PublishViewInfo, PublishViewMetaData}; +use shared_entity::dto::workspace_dto; +use shared_entity::dto::workspace_dto::ViewLayout; +use sqlx::PgPool; +use std::collections::HashSet; +use std::{collections::HashMap, sync::Arc}; +use workspace_template::gen_view_id; +use yrs::updates::encoder::Encode; +use yrs::{Map, MapRef}; + +use crate::biz::collab::ops::get_latest_collab_encoded; + +#[allow(clippy::too_many_arguments)] +pub async fn duplicate_published_collab_to_workspace( + pg_pool: &PgPool, + collab_storage: Arc, + dest_uid: i64, + publish_view_id: String, + dest_workspace_id: String, + dest_view_id: String, +) -> Result<(), AppError> { + let copier = PublishCollabDuplicator::new( + pg_pool.clone(), + collab_storage.clone(), + dest_uid, + dest_workspace_id, + dest_view_id, + ); + + let time_now = chrono::Utc::now().timestamp_millis(); + copier.duplicate(&publish_view_id).await?; + let elapsed = chrono::Utc::now().timestamp_millis() - time_now; + tracing::info!( + "duplicate_published_collab_to_workspace: elapsed time: {}ms", + elapsed + ); + Ok(()) +} + +pub struct PublishCollabDuplicator { + /// for fetching and writing folder data + /// of dest workspace + collab_storage: Arc, + /// A map to store the old view_id that was duplicated and new view_id assigned. + /// If value is none, it means the view_id is not published. + duplicated_refs: HashMap>, + /// published_database_id -> view_id + duplicated_db_main_view: HashMap, + /// published_database_view_id -> new_view_id + duplicated_db_view: HashMap, + /// new views to be added to the folder + /// view_id -> view + views_to_add: HashMap, + /// A list of database linked views to be added to workspace database + workspace_databases: HashMap>, + /// A list of collab objects to added to the workspace (oid -> collab) + collabs_to_insert: HashMap)>, + /// time of duplication + ts_now: i64, + /// for fetching published data + /// and writing them to dest workspace + pg_pool: PgPool, + /// user initiating the duplication + duplicator_uid: i64, + /// workspace to duplicate into + dest_workspace_id: String, + /// view of workspace to duplicate into + dest_view_id: String, +} + +impl PublishCollabDuplicator { + pub fn new( + pg_pool: PgPool, + collab_storage: Arc, + dest_uid: i64, + dest_workspace_id: String, + dest_view_id: String, + ) -> Self { + let ts_now = chrono::Utc::now().timestamp(); + Self { + ts_now, + duplicated_refs: HashMap::new(), + views_to_add: HashMap::new(), + workspace_databases: HashMap::new(), + collabs_to_insert: HashMap::new(), + duplicated_db_main_view: HashMap::new(), + duplicated_db_view: HashMap::new(), + + pg_pool, + collab_storage, + duplicator_uid: dest_uid, + dest_workspace_id, + dest_view_id, + } + } + + async fn duplicate(mut self, publish_view_id: &str) -> Result<(), AppError> { + // new view after deep copy + // this is the root of the document/database duplicated + let mut root_view = match self.deep_copy(gen_view_id(), publish_view_id).await? { + Some(v) => v, + None => { + return Err(AppError::RecordNotFound( + "view not found, it might be unpublished".to_string(), + )) + }, + }; + root_view.parent_view_id.clone_from(&self.dest_view_id); + + // destructuring self to own inner values, avoids cloning + let PublishCollabDuplicator { + collab_storage, + duplicated_refs: _, + duplicated_db_main_view: _, + duplicated_db_view: _, + mut views_to_add, + workspace_databases, + collabs_to_insert, + ts_now: _, + pg_pool, + duplicator_uid, + dest_workspace_id, + dest_view_id: _, + } = self; + + // insert all collab object accumulated + // for self.collabs_to_insert + let mut txn = pg_pool.begin().await?; + for (oid, (collab_type, encoded_collab)) in collabs_to_insert.into_iter() { + collab_storage + .insert_new_collab_with_transaction( + &dest_workspace_id, + &duplicator_uid, + CollabParams { + object_id: oid.clone(), + encoded_collab_v1: encoded_collab, + collab_type, + embeddings: None, + }, + &mut txn, + ) + .await?; + } + + // update database if any + if !workspace_databases.is_empty() { + let ws_db_oid = select_workspace_database_oid(&pg_pool, &dest_workspace_id.parse()?).await?; + let mut ws_db_collab = { + let ws_database_ec = get_latest_collab_encoded( + collab_storage.clone(), + &duplicator_uid, + &dest_workspace_id, + &ws_db_oid, + CollabType::WorkspaceDatabase, + ) + .await?; + collab_from_doc_state(ws_database_ec.doc_state.to_vec(), &ws_db_oid)? + }; + + let ws_db_body = WorkspaceDatabaseBody::new(&mut ws_db_collab); + + let (ws_db_updates, updated_ws_w_db_collab) = tokio::task::spawn_blocking(move || { + let ws_db_updates = { + let mut txn_wrapper = ws_db_collab.transact_mut(); + for (db_collab_id, linked_views) in &workspace_databases { + ws_db_body.add_database(&mut txn_wrapper, db_collab_id, linked_views.clone()); + } + txn_wrapper.encode_update_v1() + }; + let updated_ws_w_db_collab = collab_to_bin(&ws_db_collab, CollabType::WorkspaceDatabase); + (ws_db_updates, updated_ws_w_db_collab) + }) + .await?; + + collab_storage + .insert_new_collab_with_transaction( + &dest_workspace_id, + &duplicator_uid, + CollabParams { + object_id: ws_db_oid.clone(), + encoded_collab_v1: updated_ws_w_db_collab?, + collab_type: CollabType::WorkspaceDatabase, + embeddings: None, + }, + &mut txn, + ) + .await?; + broadcast_update(&collab_storage, &ws_db_oid, ws_db_updates).await?; + } + + let collab_folder_encoded = get_latest_collab_encoded( + collab_storage.clone(), + &duplicator_uid, + &dest_workspace_id, + &dest_workspace_id, + CollabType::Folder, + ) + .await?; + + let cloned_dest_workspace_id = dest_workspace_id.clone(); + let mut folder = tokio::task::spawn_blocking(move || { + Folder::from_collab_doc_state( + duplicator_uid, + CollabOrigin::Server, + collab_folder_encoded.into(), + &cloned_dest_workspace_id, + vec![], + ) + .map_err(|e| AppError::Unhandled(e.to_string())) + }) + .await??; + + let (encoded_update, updated_encoded_collab) = tokio::task::spawn_blocking(move || { + let encoded_update = { + let mut folder_txn = folder.collab.transact_mut(); + + let mut duplicated_view_ids = HashSet::new(); + duplicated_view_ids.insert(root_view.id.clone()); + folder.body.views.insert(&mut folder_txn, root_view, None); + + // when child views are added, it must have a parent view that is previously added + // TODO: if there are too many child views, consider using topological sort + loop { + if views_to_add.is_empty() { + break; + } + + let mut inserted = vec![]; + for (view_id, view) in views_to_add.iter() { + if duplicated_view_ids.contains(&view.parent_view_id) { + folder + .body + .views + .insert(&mut folder_txn, view.clone(), None); + duplicated_view_ids.insert(view_id.clone()); + inserted.push(view_id.clone()); + } + } + if inserted.is_empty() { + tracing::error!( + "views not inserted because parent_id does not exists: {:?}", + views_to_add.keys() + ); + break; + } + for view_id in inserted { + views_to_add.remove(&view_id); + } + } + + folder_txn.encode_update_v1() + }; + + // update folder collab + let updated_encoded_collab = collab_to_bin(&folder.collab, CollabType::Folder); + (encoded_update, updated_encoded_collab) + }) + .await?; + + collab_storage + .insert_new_collab_with_transaction( + &dest_workspace_id, + &duplicator_uid, + CollabParams { + object_id: dest_workspace_id.clone(), + encoded_collab_v1: updated_encoded_collab?, + collab_type: CollabType::Folder, + embeddings: None, + }, + &mut txn, + ) + .await?; + + // broadcast folder changes + broadcast_update(&collab_storage, &dest_workspace_id, encoded_update).await?; + + txn.commit().await?; + Ok(()) + } + + /// Deep copy a published collab to the destination workspace. + /// If None is returned, it means the view is not published. + /// If Some is returned, a new view is created but without parent_view_id set. + /// Caller should set the parent_view_id to the parent view. + async fn deep_copy( + &mut self, + new_view_id: String, + publish_view_id: &str, + ) -> Result, AppError> { + tracing::info!( + "deep_copy: new_view_id: {}, publish_view_id: {}", + new_view_id, + publish_view_id, + ); + + // attempt to get metadata and doc_state for published view + let (metadata, published_blob) = match self + .get_published_data_for_view_id(&publish_view_id.parse()?) + .await? + { + Some(published_data) => published_data, + None => { + tracing::warn!( + "No published collab data found for view_id: {}", + publish_view_id + ); + return Ok(None); + }, + }; + + // at this stage, we know that the view is published, + // so we insert this knowledge into the duplicated_refs + self + .duplicated_refs + .insert(publish_view_id.to_string(), new_view_id.clone().into()); + + match metadata.view.layout { + ViewLayout::Document => { + let doc_collab = collab_from_doc_state(published_blob, "")?; + let doc = Document::open(doc_collab).map_err(|e| AppError::Unhandled(e.to_string()))?; + let new_doc_view = self.deep_copy_doc(new_view_id, doc, metadata).await?; + Ok(Some(new_doc_view)) + }, + ViewLayout::Grid | ViewLayout::Board | ViewLayout::Calendar => { + let pub_view_id = metadata.view.view_id.clone(); + let db_payload = serde_json::from_slice::(&published_blob)?; + let new_db_view = self + .deep_copy_database_view(new_view_id, db_payload, &metadata, &pub_view_id) + .await?; + Ok(Some(new_db_view)) + }, + t => { + tracing::warn!("collab type not supported: {:?}", t); + Ok(None) + }, + } + } + + async fn deep_copy_doc<'a>( + &mut self, + new_view_id: String, + doc: Document, + metadata: PublishViewMetaData, + ) -> Result { + let mut ret_view = + self.new_folder_view(new_view_id.clone(), &metadata.view, ViewLayout::Document); + + let mut doc_data = doc + .get_document_data() + .map_err(|e| AppError::Unhandled(e.to_string()))?; + + if let Err(err) = self.deep_copy_doc_pages(&mut doc_data, &mut ret_view).await { + tracing::error!("failed to deep copy doc pages: {}", err); + } + + if let Err(err) = self + .deep_copy_doc_databases(&mut doc_data, &mut ret_view) + .await + { + tracing::error!("failed to deep copy doc databases: {}", err); + }; + + { + // write modified doc_data back to storage + let empty_collab = collab_from_doc_state(vec![], &new_view_id)?; + let new_doc = tokio::task::spawn_blocking(move || { + Document::open_with(empty_collab, Some(doc_data)) + .map_err(|e| AppError::Unhandled(e.to_string())) + }) + .await??; + let new_doc_bin = tokio::task::spawn_blocking(move || { + new_doc + .encode_collab() + .map_err(|e| AppError::Unhandled(e.to_string())) + .map(|ec| ec.encode_to_bytes()) + }) + .await?; + + self + .collabs_to_insert + .insert(ret_view.id.clone(), (CollabType::Document, new_doc_bin??)); + } + Ok(ret_view) + } + + async fn deep_copy_doc_pages( + &mut self, + doc_data: &mut DocumentData, + ret_view: &mut View, + ) -> Result<(), AppError> { + if let Some(text_map) = doc_data.meta.text_map.as_mut() { + for (_key, value) in text_map.iter_mut() { + let mut js_val = match serde_json::from_str::(value) { + Ok(js_val) => js_val, + Err(e) => { + tracing::error!("failed to parse text_map value({}): {}", value, e); + continue; + }, + }; + let js_array = match js_val.as_array_mut() { + Some(js_array) => js_array, + None => continue, + }; + + let page_ids = js_array + .iter_mut() + .flat_map(|js_val| js_val.get_mut("attributes")) + .flat_map(|attributes| attributes.get_mut("mention")) + .filter(|mention| { + mention.get("type").map_or(false, |type_| { + type_.as_str().map_or(false, |type_| type_ == "page") + }) + }) + .flat_map(|mention| mention.get_mut("page_id")); + + for page_id in page_ids { + let page_id_str = match page_id.as_str() { + Some(page_id_str) => page_id_str, + None => continue, + }; + if let Some(new_page_id) = self.deep_copy_view(page_id_str, &ret_view.id).await? { + *page_id = serde_json::json!(new_page_id); + } else { + tracing::warn!("deep_copy_doc_pages: view not found: {}", page_id_str); + }; + } + + *value = js_val.to_string(); + } + } + + Ok(()) + } + + /// attempts to deep copy a view using `view_id`. returns a new_view_id of the duplicated view. + /// if view is already duplicated, returns duplicated view's view_id (parent_view_id is not set + /// from param `parent_view_id`) + async fn deep_copy_view( + &mut self, + view_id: &str, + parent_view_id: &String, + ) -> Result, AppError> { + match self.duplicated_refs.get(view_id) { + Some(new_view_id) => { + if let Some(vid) = new_view_id { + Ok(Some(vid.clone())) + } else { + Ok(None) + } + }, + None => { + // Call deep_copy and await the result + if let Some(mut new_view) = Box::pin(self.deep_copy(gen_view_id(), view_id)).await? { + if new_view.parent_view_id.is_empty() { + new_view.parent_view_id.clone_from(parent_view_id); + } + self + .duplicated_refs + .insert(view_id.to_string(), Some(new_view.id.clone())); + let ret_view_id = new_view.id.clone(); + self.views_to_add.insert(new_view.id.clone(), new_view); + Ok(Some(ret_view_id)) + } else { + tracing::warn!("view not found in deep_copy: {}", view_id); + self.duplicated_refs.insert(view_id.to_string(), None); + Ok(None) + } + }, + } + } + + async fn deep_copy_doc_databases( + &mut self, + doc_data: &mut DocumentData, + ret_view: &mut View, + ) -> Result<(), AppError> { + let db_blocks = doc_data + .blocks + .iter_mut() + .filter(|(_, b)| b.ty == "grid" || b.ty == "board" || b.ty == "calendar"); + + for (block_id, block) in db_blocks { + tracing::info!("deep_copy_doc_databases: block_id: {}", block_id); + let block_view_id = block + .data + .get("view_id") + .ok_or_else(|| AppError::RecordNotFound("view_id not found in block data".to_string()))? + .as_str() + .ok_or_else(|| AppError::RecordNotFound("view_id not a string".to_string()))?; + + let block_parent_id = block + .data + .get("parent_id") + .ok_or_else(|| AppError::RecordNotFound("view_id not found in block data".to_string()))? + .as_str() + .ok_or_else(|| AppError::RecordNotFound("view_id not a string".to_string()))?; + + if let Some((new_view_id, new_parent_id)) = self + .deep_copy_database_inline_doc(block_view_id, block_parent_id, &ret_view.id) + .await? + { + block.data.insert( + "view_id".to_string(), + serde_json::Value::String(new_view_id), + ); + block.data.insert( + "parent_id".to_string(), + serde_json::Value::String(new_parent_id), + ); + } else { + tracing::warn!("deep_copy_doc_databases: view not found: {}", block_view_id); + } + } + + Ok(()) + } + + /// deep copy database for doc + /// returns new (view_id, parent_id) + async fn deep_copy_database_inline_doc<'a>( + &mut self, + view_id: &str, + parent_id: &str, + doc_view_id: &String, + ) -> Result, AppError> { + let (metadata, published_blob) = match self + .get_published_data_for_view_id(&view_id.parse()?) + .await? + { + Some(published_data) => published_data, + None => { + tracing::warn!("No published collab data found for view_id: {}", view_id); + return Ok(None); + }, + }; + + let published_db = serde_json::from_slice::(&published_blob)?; + let mut parent_view = self + .deep_copy_database_view(gen_view_id(), published_db, &metadata, parent_id) + .await?; + let parent_view_id = parent_view.id.clone(); + if parent_view.parent_view_id.is_empty() { + parent_view.parent_view_id.clone_from(doc_view_id); + self + .views_to_add + .insert(parent_view.id.clone(), parent_view); + } + let duplicated_view_id = match self.duplicated_db_view.get(view_id) { + Some(v) => v.clone(), + None => { + let view_info_by_id = view_info_by_view_id(&metadata); + let view_info = view_info_by_id.get(view_id).ok_or_else(|| { + AppError::RecordNotFound(format!("metadata not found for view: {}", view_id)) + })?; + let mut new_folder_db_view = + self.new_folder_view(view_id.to_string(), view_info, view_info.layout.clone()); + new_folder_db_view.parent_view_id = parent_view_id.clone(); + let new_folder_db_view_id = new_folder_db_view.id.clone(); + self + .views_to_add + .insert(new_folder_db_view.id.clone(), new_folder_db_view); + new_folder_db_view_id + }, + }; + Ok(Some((duplicated_view_id, parent_view_id))) + } + + /// Deep copy a published database (does not create folder views) + /// checks if database is already published + /// attempts to use `new_view_id` for `published_view_id` if not already published + /// stores all view_id references in `duplicated_refs` + /// returns (published_db_id, new_db_id, is_already_duplicated) + async fn deep_copy_database<'a>( + &mut self, + published_db: &PublishDatabaseData, + publish_view_id: &str, + new_view_id: String, + ) -> Result<(String, String, bool), AppError> { + // collab of database + let mut db_collab = collab_from_doc_state(published_db.database_collab.clone(), "")?; + let pub_db_id = get_database_id_from_collab(&db_collab)?; + + // check if the database is already duplicated + if let Some(db_id) = self.duplicated_refs.get(&pub_db_id).cloned().flatten() { + return Ok((pub_db_id, db_id, true)); + } + + let new_db_id = gen_view_id(); + self + .duplicated_refs + .insert(pub_db_id.clone(), Some(new_db_id.clone())); + + // duplicate db collab rows + for (old_id, row_bin_data) in &published_db.database_row_collabs { + // assign a new id for the row + let new_row_id = gen_view_id(); + let mut db_row_collab = collab_from_doc_state(row_bin_data.clone(), &new_row_id)?; + + { + // update database_id and row_id in data + let mut txn = db_row_collab.context.transact_mut(); + let data = db_row_collab + .data + .get(&txn, "data") + .ok_or_else(|| { + AppError::RecordNotFound("no data found in database row collab".to_string()) + })? + .cast::() + .map_err(|err| AppError::Unhandled(format!("data not map: {:?}", err)))?; + data.insert(&mut txn, "id", new_row_id.clone()); + data.insert(&mut txn, "database_id", new_db_id.clone()); + } + + // write new row collab to storage + let db_row_ec_bytes = + tokio::task::spawn_blocking(move || collab_to_bin(&db_row_collab, CollabType::DatabaseRow)) + .await?; + self.collabs_to_insert.insert( + new_row_id.clone(), + (CollabType::DatabaseRow, db_row_ec_bytes?), + ); + self + .duplicated_refs + .insert(old_id.clone(), Some(new_row_id)); + } + + // accumulate list of database views (Board, Cal, ...) to be linked to the database + let mut new_db_view_ids: Vec = vec![]; + { + let mut txn = db_collab.context.transact_mut(); + let container = db_collab + .data + .get(&txn, "database") + .ok_or_else(|| AppError::RecordNotFound("no database found in collab".to_string()))? + .cast::() + .map_err(|err| AppError::Unhandled(format!("not a map: {:?}", err)))?; + container.insert(&mut txn, "id", new_db_id.clone()); + + let view_map = { + let map_ref = db_collab + .data + .get_with_path(&txn, ["database", "views"]) + .ok_or_else(|| AppError::RecordNotFound("no views found in database".to_string()))?; + ViewMap::new(map_ref, tokio::sync::broadcast::channel(1).0) + }; + + // create new database views based on published views + let mut db_views = view_map.get_all_views(&txn); + + for db_view in db_views.iter_mut() { + let new_view_id = if db_view.id == publish_view_id { + self + .duplicated_db_main_view + .insert(pub_db_id.clone(), new_view_id.clone()); + new_view_id.clone() + } else { + gen_view_id() + }; + self + .duplicated_db_view + .insert(db_view.id.clone(), new_view_id.clone()); + + db_view.id.clone_from(&new_view_id); + db_view.database_id.clone_from(&new_db_id); + new_db_view_ids.push(db_view.id.clone()); + + // update all views's row's id + for row_order in db_view.row_orders.iter_mut() { + if let Some(new_id) = self + .duplicated_refs + .get(row_order.id.as_str()) + .cloned() + .flatten() + { + row_order.id = new_id.into(); + } else { + // skip if row not found + tracing::warn!("row not found: {}", row_order.id); + continue; + } + } + } + + // insert updated views back to db + view_map.clear(&mut txn); + for view in db_views { + view_map.insert_view(&mut txn, view); + } + } + + // write database collab to storage + let db_encoded_collab = + tokio::task::spawn_blocking(move || collab_to_bin(&db_collab, CollabType::Database)).await?; + self.collabs_to_insert.insert( + new_db_id.clone(), + (CollabType::Database, db_encoded_collab?), + ); + + // Add this database as linked view + self + .workspace_databases + .insert(new_db_id.clone(), new_db_view_ids); + + Ok((pub_db_id, new_db_id, false)) + } + + /// Deep copy a published database to the destination workspace. + /// Returns the Folder view for main view (`new_view_id`) and map from old to new view_id. + /// If the database is already duplicated before, does not return the view with `new_view_id` + async fn deep_copy_database_view<'a>( + &mut self, + new_view_id: String, + published_db: PublishDatabaseData, + metadata: &PublishViewMetaData, + pub_view_id: &str, + ) -> Result { + // flatten nested view info into a map + let view_info_by_id = view_info_by_view_id(metadata); + + let (pub_db_id, _dup_db_id, db_alr_duplicated) = self + .deep_copy_database(&published_db, pub_view_id, new_view_id) + .await?; + + if db_alr_duplicated { + let duplicated_view_id = self + .duplicated_db_view + .get(pub_view_id) + .cloned() + .ok_or_else(|| AppError::RecordNotFound(format!("view not found: {}", pub_view_id)))?; + + // db_view_id found but may not have been created due to visibility + match self.views_to_add.get(&duplicated_view_id) { + Some(v) => return Ok(v.clone()), + None => { + let main_view_id = self + .duplicated_db_main_view + .get(pub_db_id.as_str()) + .ok_or_else(|| { + AppError::RecordNotFound(format!("main view not found: {}", pub_view_id)) + })?; + + let view_info = view_info_by_id.get(main_view_id).ok_or_else(|| { + AppError::RecordNotFound(format!("metadata not found for view: {}", main_view_id)) + })?; + + let mut view = + self.new_folder_view(duplicated_view_id, view_info, view_info.layout.clone()); + view.parent_view_id.clone_from(main_view_id); + return Ok(view); + }, + }; + } else { + tracing::warn!("database not duplicated: {}", pub_view_id); + } + + // create a new view to be returned to the caller + // view_id is the main view of the database + // create the main view + let main_view_id = self + .duplicated_db_main_view + .get(pub_db_id.as_str()) + .ok_or_else(|| AppError::RecordNotFound(format!("main view not found: {}", pub_view_id)))?; + + let main_view_info = view_info_by_id.get(pub_view_id).ok_or_else(|| { + AppError::RecordNotFound(format!("metadata not found for view: {}", pub_view_id)) + })?; + let main_folder_view = self.new_folder_view( + main_view_id.clone(), + main_view_info, + main_view_info.layout.clone(), + ); + + // create other visible view which are child to the main view + for vis_view_id in published_db.visible_database_view_ids { + if vis_view_id == pub_view_id { + // skip main view + continue; + } + + let child_view_id = self + .duplicated_db_view + .get(&vis_view_id) + .ok_or_else(|| AppError::RecordNotFound(format!("view not found: {}", vis_view_id)))?; + + let child_view_info = view_info_by_id.get(&vis_view_id).ok_or_else(|| { + AppError::RecordNotFound(format!("metadata not found for view: {}", vis_view_id)) + })?; + + let mut child_folder_view = self.new_folder_view( + child_view_id.clone(), + view_info_by_id.get(&vis_view_id).ok_or_else(|| { + AppError::RecordNotFound(format!("metadata not found for view: {}", vis_view_id)) + })?, + child_view_info.layout.clone(), + ); + child_folder_view.parent_view_id.clone_from(main_view_id); + self + .views_to_add + .insert(child_folder_view.id.clone(), child_folder_view); + } + + Ok(main_folder_view) + } + + /// ceates a new folder view without parent_view_id set + fn new_folder_view( + &self, + new_view_id: String, + view_info: &PublishViewInfo, + layout: ViewLayout, + ) -> View { + View { + id: new_view_id, + parent_view_id: "".to_string(), // to be filled by caller + name: view_info.name.clone(), + desc: "".to_string(), // unable to get from metadata + children: RepeatedViewIdentifier { items: vec![] }, // fill in while iterating children + created_at: self.ts_now, + is_favorite: false, + layout: to_folder_view_layout(layout), + icon: view_info.icon.clone().map(to_folder_view_icon), + created_by: Some(self.duplicator_uid), + last_edited_time: self.ts_now, + last_edited_by: Some(self.duplicator_uid), + extra: view_info.extra.clone(), + } + } + + async fn get_published_data_for_view_id( + &self, + view_id: &uuid::Uuid, + ) -> Result)>, AppError> { + match select_published_data_for_view_id(&self.pg_pool, view_id).await? { + Some((js_val, blob)) => { + let metadata = serde_json::from_value(js_val)?; + Ok(Some((metadata, blob))) + }, + None => Ok(None), + } + } +} + +/// broadcast updates to collab group if exists +async fn broadcast_update( + collab_storage: &Arc, + oid: &str, + encoded_update: Vec, +) -> Result<(), AppError> { + tracing::info!("broadcasting update to group: {}", oid); + + let payload = Message::Sync(SyncMessage::Update(encoded_update)).encode_v1(); + let msg = ClientCollabMessage::ClientUpdateSync { + data: UpdateSync { + origin: CollabOrigin::Server, + object_id: oid.to_string(), + msg_id: chrono::Utc::now().timestamp_millis() as u64, + payload: payload.into(), + }, + }; + + collab_storage + .broadcast_encode_collab(oid.to_string(), vec![msg]) + .await?; + + Ok(()) +} + +fn view_info_by_view_id(meta: &PublishViewMetaData) -> HashMap { + let mut acc = HashMap::new(); + acc.insert(meta.view.view_id.clone(), meta.view.clone()); + add_to_view_info(&mut acc, &meta.child_views); + add_to_view_info(&mut acc, &meta.ancestor_views); + acc +} + +fn add_to_view_info(acc: &mut HashMap, view_infos: &[PublishViewInfo]) { + for view_info in view_infos { + acc.insert(view_info.view_id.clone(), view_info.clone()); + if let Some(child_views) = &view_info.child_views { + add_to_view_info(acc, child_views); + } + } +} + +pub fn collab_from_doc_state(doc_state: Vec, object_id: &str) -> Result { + let collab = Collab::new_with_source( + CollabOrigin::Server, + object_id, + DataSource::DocStateV1(doc_state), + vec![], + false, + ) + .map_err(|e| AppError::Unhandled(e.to_string()))?; + Ok(collab) +} + +pub fn get_database_id_from_collab(db_collab: &Collab) -> Result { + let txn = db_collab.context.transact(); + let db_map = db_collab + .get_with_txn(&txn, "database") + .ok_or_else(|| AppError::RecordNotFound("no database found in database collab".to_string()))? + .cast::() + .map_err(|err| AppError::RecordNotFound(format!("database not a map: {:?}", err)))?; + let db_id = db_map + .get(&txn, "id") + .ok_or_else(|| AppError::RecordNotFound("no id found in database".to_string()))? + .to_string(&txn); + Ok(db_id) +} + +fn to_folder_view_icon(icon: workspace_dto::ViewIcon) -> collab_folder::ViewIcon { + collab_folder::ViewIcon { + ty: to_folder_view_icon_type(icon.ty), + value: icon.value, + } +} + +fn to_folder_view_icon_type(icon: workspace_dto::IconType) -> collab_folder::IconType { + match icon { + workspace_dto::IconType::Emoji => collab_folder::IconType::Emoji, + workspace_dto::IconType::Url => collab_folder::IconType::Url, + workspace_dto::IconType::Icon => collab_folder::IconType::Icon, + } +} + +fn to_folder_view_layout(layout: workspace_dto::ViewLayout) -> collab_folder::ViewLayout { + match layout { + ViewLayout::Document => collab_folder::ViewLayout::Document, + ViewLayout::Grid => collab_folder::ViewLayout::Grid, + ViewLayout::Board => collab_folder::ViewLayout::Board, + ViewLayout::Calendar => collab_folder::ViewLayout::Calendar, + ViewLayout::Chat => collab_folder::ViewLayout::Chat, + } +} + +fn collab_to_bin(collab: &Collab, collab_type: CollabType) -> Result, AppError> { + let bin = collab + .encode_collab_v1(|collab| collab_type.validate_require_data(collab)) + .map_err(|e| AppError::Unhandled(e.to_string()))? + .encode_to_bytes()?; + Ok(bin) +} diff --git a/src/telemetry.rs b/src/telemetry.rs index f1cfae2d3..f0914a362 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -15,7 +15,8 @@ pub fn init_subscriber(app_env: &Environment, filters: Vec) { .with_target(true) .with_max_level(tracing::Level::TRACE) .with_thread_ids(false) - .with_file(false); + .with_file(true) + .with_line_number(true); match app_env { Environment::Local => { diff --git a/tests/workspace/mod.rs b/tests/workspace/mod.rs index 0d3552f28..21a177abb 100644 --- a/tests/workspace/mod.rs +++ b/tests/workspace/mod.rs @@ -3,6 +3,8 @@ mod edit_workspace; mod invitation_crud; mod member_crud; mod publish; +mod published_data; mod template; mod workspace_crud; +mod workspace_folder; mod workspace_settings; diff --git a/tests/workspace/publish.rs b/tests/workspace/publish.rs index ef2fe12cf..8f901d2ee 100644 --- a/tests/workspace/publish.rs +++ b/tests/workspace/publish.rs @@ -1,13 +1,29 @@ -use std::collections::HashMap; +use appflowy_cloud::biz::collab::folder_view::collab_folder_to_folder_view; +use appflowy_cloud::biz::workspace::publish_dup::{ + collab_from_doc_state, get_database_id_from_collab, +}; +use collab::util::MapExt; +use collab_database::views::ViewMap; +use collab_database::workspace_database::WorkspaceDatabaseBody; +use collab_entity::CollabType; +use collab_folder::{CollabOrigin, Folder}; +use shared_entity::dto::publish_dto::PublishDatabaseData; +use shared_entity::dto::publish_dto::PublishViewMetaData; +use shared_entity::dto::workspace_dto::PublishedDuplicate; +use std::collections::{HashMap, HashSet}; use std::thread::sleep; use std::time::Duration; use app_error::ErrorCode; -use client_api::entity::{AFRole, GlobalComment, PublishCollabItem, PublishCollabMetadata}; +use client_api::entity::{ + AFRole, GlobalComment, PublishCollabItem, PublishCollabMetadata, QueryCollab, QueryCollabParams, +}; use client_api_test::TestClient; use client_api_test::{generate_unique_registered_user_client, localhost_client}; use itertools::Itertools; +use crate::workspace::published_data::{self}; + #[tokio::test] async fn test_set_publish_namespace_set() { let (c, _user) = generate_unique_registered_user_client().await; @@ -661,3 +677,318 @@ async fn workspace_member_publish_unpublish() { struct MyCustomMetadata { title: String, } + +#[tokio::test] +async fn duplicate_to_workspace_references() { + let client_1 = TestClient::new_user().await; + let workspace_id = client_1.workspace_id().await; + + // doc2 contains a reference to doc1 + let doc_2_view_id = uuid::Uuid::new_v4(); + let doc_2_metadata: PublishViewMetaData = + serde_json::from_str(published_data::DOC_2_META).unwrap(); + let doc_2_doc_state = hex::decode(published_data::DOC_2_DOC_STATE_HEX).unwrap(); + + // doc_1_view_id needs to be fixed because doc_2 references it + let doc_1_view_id: uuid::Uuid = "e8c4f99a-50ea-4758-bca0-afa7df5c2434".parse().unwrap(); + let doc_1_metadata: PublishViewMetaData = + serde_json::from_str(published_data::DOC_1_META).unwrap(); + let doc_1_doc_state = hex::decode(published_data::DOC_1_DOC_STATE_HEX).unwrap(); + + // doc1 contains @reference database to grid1 (not inline) + let grid_1_view_id: uuid::Uuid = "8e062f61-d7ae-4f4b-869c-f44c43149399".parse().unwrap(); + let grid_1_metadata: PublishViewMetaData = + serde_json::from_str(published_data::GRID_1_META).unwrap(); + let grid_1_db_data = hex::decode(published_data::GRID_1_DB_DATA).unwrap(); + + client_1 + .api_client + .publish_collabs( + &workspace_id, + vec![ + PublishCollabItem { + meta: PublishCollabMetadata { + view_id: doc_1_view_id, + publish_name: doc_1_metadata.view.name.clone(), + metadata: doc_1_metadata.clone(), + }, + data: doc_1_doc_state, + }, + PublishCollabItem { + meta: PublishCollabMetadata { + view_id: doc_2_view_id, + publish_name: doc_2_metadata.view.name.clone(), + metadata: doc_2_metadata.clone(), + }, + data: doc_2_doc_state, + }, + PublishCollabItem { + meta: PublishCollabMetadata { + view_id: grid_1_view_id, + publish_name: grid_1_metadata.view.name.clone(), + metadata: grid_1_metadata.clone(), + }, + data: grid_1_db_data, + }, + ], + ) + .await + .unwrap(); + + { + let client_2 = TestClient::new_user().await; + let workspace_id_2 = client_2.workspace_id().await; + let fv = client_2 + .api_client + .get_workspace_folder(&workspace_id_2, Some(5)) + .await + .unwrap(); + + // duplicate doc2 to workspace2 + // Result fv should be: + // . + // ├── Getting Started (existing) + // └── doc2 + // └── doc1 + // └── grid1 + client_2 + .api_client + .duplicate_published_to_workspace( + &workspace_id_2, + &PublishedDuplicate { + published_view_id: doc_2_view_id.to_string(), + dest_view_id: fv.view_id, // use the root view + }, + ) + .await + .unwrap(); + + let fv = client_2 + .api_client + .get_workspace_folder(&workspace_id_2, Some(5)) + .await + .unwrap(); + + let doc_2_fv = fv + .children + .into_iter() + .find(|v| v.name == doc_2_metadata.view.name) + .unwrap(); + assert_ne!(doc_2_fv.view_id, doc_1_view_id.to_string()); + + let doc_1_fv = doc_2_fv + .children + .into_iter() + .find(|v| v.name == doc_1_metadata.view.name) + .unwrap(); + assert_ne!(doc_1_fv.view_id, doc_1_view_id.to_string()); + + let grid_1_fv = doc_1_fv + .children + .into_iter() + .find(|v| v.name == grid_1_metadata.view.name) + .unwrap(); + assert_ne!(grid_1_fv.view_id, grid_1_view_id.to_string()); + } +} + +#[tokio::test] +async fn duplicate_to_workspace_doc_inline_database() { + let client_1 = TestClient::new_user().await; + let workspace_id = client_1.workspace_id().await; + + // doc3 contains inline database to a view in grid1 (view of grid1) + let doc_3_view_id = uuid::Uuid::new_v4(); + let doc_3_metadata: PublishViewMetaData = + serde_json::from_str(published_data::DOC_3_META).unwrap(); + let doc_3_doc_state = hex::decode(published_data::DOC_3_DOC_STATE_HEX).unwrap(); + + // view of grid1 + let view_of_grid_1_view_id: uuid::Uuid = "d8589e98-88fc-42e4-888c-b03338bf22bb".parse().unwrap(); + let view_of_grid_1_metadata: PublishViewMetaData = + serde_json::from_str(published_data::VIEW_OF_GRID1_META).unwrap(); + let view_of_grid_1_db_data = hex::decode(published_data::VIEW_OF_GRID_1_DB_DATA).unwrap(); + let (pub_db_id, pub_row_ids) = get_database_id_and_row_ids(&view_of_grid_1_db_data); + + client_1 + .api_client + .publish_collabs( + &workspace_id, + vec![ + PublishCollabItem { + meta: PublishCollabMetadata { + view_id: doc_3_view_id, + publish_name: doc_3_metadata.view.name.clone(), + metadata: doc_3_metadata.clone(), + }, + data: doc_3_doc_state, + }, + PublishCollabItem { + meta: PublishCollabMetadata { + view_id: view_of_grid_1_view_id, + publish_name: view_of_grid_1_metadata.view.name.replace(' ', "-"), + metadata: view_of_grid_1_metadata.clone(), + }, + data: view_of_grid_1_db_data, + }, + ], + ) + .await + .unwrap(); + + { + let mut client_2 = TestClient::new_user().await; + let workspace_id_2 = client_2.workspace_id().await; + + // Open workspace to trigger group creation + client_2 + .open_collab(&workspace_id_2, &workspace_id_2, CollabType::Folder) + .await; + + let fv = client_2 + .api_client + .get_workspace_folder(&workspace_id_2, Some(5)) + .await + .unwrap(); + + // duplicate doc3 to workspace2 + // Result fv should be: + // . + // ├── Getting Started (existing) + // └── doc3 + // └── grid1 + // └── View of grid1 + client_2 + .api_client + .duplicate_published_to_workspace( + &workspace_id_2, + &PublishedDuplicate { + published_view_id: doc_3_view_id.to_string(), + dest_view_id: fv.view_id, // use the root view + }, + ) + .await + .unwrap(); + + { + let fv = client_2 + .api_client + .get_workspace_folder(&workspace_id_2, Some(5)) + .await + .unwrap(); + let doc_3_fv = fv + .children + .into_iter() + .find(|v| v.name == doc_3_metadata.view.name) + .unwrap(); + let grid1_fv = doc_3_fv + .children + .into_iter() + .find(|v| v.name == "grid1") + .unwrap(); + let _view_of_grid1_fv = grid1_fv + .children + .into_iter() + .find(|v| v.name == "View of grid1") + .unwrap(); + } + + let collab_resp = client_2 + .get_collab(QueryCollabParams { + workspace_id: workspace_id_2.clone(), + inner: QueryCollab { + object_id: workspace_id_2.clone(), + collab_type: CollabType::Folder, + }, + }) + .await + .unwrap(); + + let folder = Folder::from_collab_doc_state( + client_2.uid().await, + CollabOrigin::Server, + collab_resp.encode_collab.into(), + &workspace_id_2, + vec![], + ) + .unwrap(); + + let folder_view = collab_folder_to_folder_view(&folder, 5); + let doc_3_fv = folder_view + .children + .into_iter() + .find(|v| v.name == doc_3_metadata.view.name) + .unwrap(); + assert_ne!(doc_3_fv.view_id, doc_3_view_id.to_string()); + + let grid1_fv = doc_3_fv + .children + .into_iter() + .find(|v| v.name == "grid1") + .unwrap(); + assert_ne!(grid1_fv.view_id, view_of_grid_1_view_id.to_string()); + + let view_of_grid1_fv = grid1_fv + .children + .into_iter() + .find(|v| v.name == "View of grid1") + .unwrap(); + println!("{:#?}", view_of_grid1_fv); + assert_ne!(view_of_grid1_fv.view_id, view_of_grid_1_view_id.to_string()); + + { + // check that database_id is different + let mut ws_db_collab = client_2 + .get_workspace_database_collab(&workspace_id_2) + .await; + let ws_db_body = WorkspaceDatabaseBody::new(&mut ws_db_collab); + let txn = ws_db_collab.transact(); + let dup_grid1_db_id = ws_db_body + .get_all_database_meta(&txn) + .into_iter() + .find(|db_meta| db_meta.linked_views.contains(&view_of_grid1_fv.view_id)) + .unwrap() + .database_id; + let db_collab_collab_resp = client_2 + .get_collab(QueryCollabParams { + workspace_id: workspace_id_2, + inner: QueryCollab { + object_id: dup_grid1_db_id, + collab_type: CollabType::Database, + }, + }) + .await + .unwrap(); + let db_doc_state = db_collab_collab_resp.encode_collab.doc_state; + let db_collab = collab_from_doc_state(db_doc_state.to_vec(), "").unwrap(); + let dup_db_id = get_database_id_from_collab(&db_collab).unwrap(); + assert_ne!(dup_db_id, pub_db_id); + + let view_map = { + let map_ref = db_collab + .data + .get_with_path(&txn, ["database", "views"]) + .unwrap(); + ViewMap::new(map_ref, tokio::sync::broadcast::channel(1).0) + }; + + for db_view in view_map.get_all_views(&txn) { + assert_eq!(db_view.database_id, dup_db_id); + for row_order in db_view.row_orders { + assert!( + !pub_row_ids.contains(row_order.id.as_str()), + "published row id is same as duplicated row id" + ); + } + } + } + } +} + +fn get_database_id_and_row_ids(published_db_blob: &[u8]) -> (String, HashSet) { + let pub_db_data = serde_json::from_slice::(published_db_blob).unwrap(); + let db_collab = collab_from_doc_state(pub_db_data.database_collab, "").unwrap(); + let pub_db_id = get_database_id_from_collab(&db_collab).unwrap(); + let row_ids: HashSet = pub_db_data.database_row_collabs.into_keys().collect(); + (pub_db_id, row_ids) +} diff --git a/tests/workspace/published_data.rs b/tests/workspace/published_data.rs new file mode 100644 index 000000000..6f0ff3e81 --- /dev/null +++ b/tests/workspace/published_data.rs @@ -0,0 +1,350 @@ +pub const DOC_1_META: &str = r#" + { + "view": { + "icon": { + "ty": 0, + "value": "🍚" + }, + "name": "doc1", + "extra": "{\"cover\":{\"type\":\"none\",\"value\":\"\"}}", + "layout": 0, + "view_id": "e8c4f99a-50ea-4758-bca0-afa7df5c2434", + "created_at": 1724143304, + "created_by": 311828434584080400, + "child_views": null, + "last_edited_by": 311828434584080400, + "last_edited_time": 1724146846 + }, + "child_views": [], + "ancestor_views": [ + { + "icon": null, + "name": "Workspace", + "extra": null, + "layout": 0, + "view_id": "043832c3-c9c4-40e8-ae02-e25677ef344f", + "created_at": 1724143277, + "created_by": 311828434584080400, + "child_views": null, + "last_edited_by": 311828434584080400, + "last_edited_time": 1724143277 + }, + { + "icon": null, + "name": "Shared", + "extra": "{\"is_space\":true,\"space_icon\":\"space_icon_1\",\"space_icon_color\":\"0xFFA34AFD\",\"space_permission\":0,\"space_created_at\":1724143277323}", + "layout": 0, + "view_id": "52adbe8e-57c1-43e9-8ef0-e7d49618aa27", + "created_at": 1724146819, + "created_by": 311828434584080400, + "child_views": null, + "last_edited_by": 311828434584080400, + "last_edited_time": 1724146819 + }, + { + "icon": { + "ty": 0, + "value": "🍚" + }, + "name": "doc1", + "extra": "{\"cover\":{\"type\":\"none\",\"value\":\"\"}}", + "layout": 0, + "view_id": "e8c4f99a-50ea-4758-bca0-afa7df5c2434", + "created_at": 1724143304, + "created_by": 311828434584080400, + "child_views": null, + "last_edited_by": 311828434584080400, + "last_edited_time": 1724146846 + } + ] + } +"#; + +pub const DOC_1_DOC_STATE_HEX: &str = "050faafac5b509002700edea9f92040406367975544b5f022700edea9f920401067042476d6f41012800aafac5b509010269640177067042476d6f412800aafac5b509010274790177097061726167726170682800aafac5b5090106706172656e7401772434616236646431662d353866342d353033632d613533642d3465383038336133373263662800aafac5b50901086368696c6472656e017706623030782d4d2800aafac5b5090104646174610177027b7d2800aafac5b509010b65787465726e616c5f6964017706367975544b5f2800aafac5b509010d65787465726e616c5f74797065017704746578742700edea9f92040306623030782d4d0088edea9f9204180177067042476d6f410100aafac5b509000a86aafac5b50914076d656e74696f6e407b2274797065223a2270616765222c22706167655f6964223a2238653036326636312d643761652d346634622d383639632d663434633433313439333939227d84aafac5b50915012486aafac5b50916076d656e74696f6e046e756c6c03b1dfade005000400edea9f92041905646f63312081b1dfade005040284b1dfade005060673747566667302d380919c04002101046d6574610c6c6173745f73796e635f617430a8d380919c042f017d9ebfa3ec0c1aedea9f9204002701046461746108646f63756d656e74012700edea9f92040006626c6f636b73012700edea9f920400046d657461012700edea9f9204020c6368696c6472656e5f6d6170012700edea9f92040208746578745f6d6170012800edea9f92040007706167655f696401772434616236646431662d353866342d353033632d613533642d3465383038336133373263662700edea9f9204012434616236646431662d353866342d353033632d613533642d346538303833613337326366012800edea9f92040602696401772434616236646431662d353866342d353033632d613533642d3465383038336133373263662800edea9f920406027479017704706167652800edea9f92040606706172656e740177002800edea9f920406086368696c6472656e01772434616236646431662d353866342d353033632d613533642d3465383038336133373263662800edea9f92040604646174610177027b7d2800edea9f9204060b65787465726e616c5f6964017e2800edea9f9204060d65787465726e616c5f74797065017e2700edea9f9204032434616236646431662d353866342d353033632d613533642d346538303833613337326366002700edea9f9204010a65676e6d5f7341724f33012800edea9f92040f02696401770a65676e6d5f7341724f332800edea9f92040f0274790177097061726167726170682800edea9f92040f06706172656e7401772434616236646431662d353866342d353033632d613533642d3465383038336133373263662800edea9f92040f086368696c6472656e01770a666639562d2d384c77722800edea9f92040f04646174610177027b7d2800edea9f92040f0b65787465726e616c5f696401770a6d59565a5978314579682800edea9f92040f0d65787465726e616c5f74797065017704746578742700edea9f9204030a666639562d2d384c7772000800edea9f92040e01770a65676e6d5f7341724f332700edea9f9204040a6d59565a5978314579680201ed9a8cf603002101046d6574610c6c6173745f73796e635f61742804b1dfade005010502aafac5b509010b0ad380919c04010030ed9a8cf603010028"; + +pub const DOC_2_META: &str = r#" +{ + "view": { + "icon": { + "ty": 0, + "value": "🇧🇼" + }, + "name": "doc2", + "extra": "{\"cover\":{\"type\":\"none\",\"value\":\"\"}}", + "layout": 0, + "view_id": "9234eaf0-30ee-4edf-b503-e76869d584e5", + "created_at": 1724143313, + "created_by": 311828434584080400, + "child_views": null, + "last_edited_by": 311828434584080400, + "last_edited_time": 1724143963 + }, + "child_views": [], + "ancestor_views": [ + { + "icon": null, + "name": "Workspace", + "extra": null, + "layout": 0, + "view_id": "043832c3-c9c4-40e8-ae02-e25677ef344f", + "created_at": 1724143277, + "created_by": 311828434584080400, + "child_views": null, + "last_edited_by": 311828434584080400, + "last_edited_time": 1724143277 + }, + { + "icon": null, + "name": "Shared", + "extra": "{\"is_space\":true,\"space_icon\":\"space_icon_1\",\"space_icon_color\":\"0xFFA34AFD\",\"space_permission\":0,\"space_created_at\":1724143277323}", + "layout": 0, + "view_id": "52adbe8e-57c1-43e9-8ef0-e7d49618aa27", + "created_at": 1724143313, + "created_by": 311828434584080400, + "child_views": null, + "last_edited_by": 311828434584080400, + "last_edited_time": 1724143313 + }, + { + "icon": { + "ty": 0, + "value": "🇧🇼" + }, + "name": "doc2", + "extra": "{\"cover\":{\"type\":\"none\",\"value\":\"\"}}", + "layout": 0, + "view_id": "9234eaf0-30ee-4edf-b503-e76869d584e5", + "created_at": 1724143313, + "created_by": 311828434584080400, + "child_views": null, + "last_edited_by": 311828434584080400, + "last_edited_time": 1724143963 + } + ] +} +"#; + +pub const DOC_2_DOC_STATE_HEX: &str = "03048f99d3810d00010096d0a384091904868f99d3810d03076d656e74696f6e407b2274797065223a2270616765222c22706167655f6964223a2265386334663939612d353065612d343735382d626361302d616661376466356332343334227d848f99d3810d040124868f99d3810d05076d656e74696f6e046e756c6c028a99c3a80b002101046d6574610c6c6173745f73796e635f617427a88a99c3a80b26017d91e5a2ec0c1a96d0a38409002701046461746108646f63756d656e7401270096d0a384090006626c6f636b7301270096d0a3840900046d65746101270096d0a38409020c6368696c6472656e5f6d617001270096d0a384090208746578745f6d617001280096d0a384090007706167655f696401772439663235353130332d656663372d353136612d613637352d383330633561313931393931270096d0a38409010a356d593767344e70477601280096d0a384090602696401770a356d593767344e704776280096d0a3840906027479017709706172616772617068280096d0a384090606706172656e7401772439663235353130332d656663372d353136612d613637352d383330633561313931393931280096d0a3840906086368696c6472656e01770a36413051464d6a356842280096d0a384090604646174610177027b7d280096d0a38409060b65787465726e616c5f696401770a4946763557524e774138280096d0a38409060d65787465726e616c5f7479706501770474657874270096d0a38409030a36413051464d6a35684200270096d0a38409012439663235353130332d656663372d353136612d613637352d38333063356131393139393101280096d0a384090f02696401772439663235353130332d656663372d353136612d613637352d383330633561313931393931280096d0a384090f02747901770470616765280096d0a384090f06706172656e74017700280096d0a384090f086368696c6472656e01772439663235353130332d656663372d353136612d613637352d383330633561313931393931280096d0a384090f04646174610177027b7d280096d0a384090f0b65787465726e616c5f6964017e280096d0a384090f0d65787465726e616c5f74797065017e270096d0a38409032439663235353130332d656663372d353136612d613637352d38333063356131393139393100080096d0a384091701770a356d593767344e704776270096d0a38409040a4946763557524e77413802028a99c3a80b0100278f99d3810d010004"; + +pub const GRID_1_META: &str = r#" +{ + "view": { + "icon": { + "ty": 0, + "value": "🍽️" + }, + "name": "grid1", + "extra": null, + "layout": 1, + "view_id": "8e062f61-d7ae-4f4b-869c-f44c43149399", + "created_at": 1724146952, + "created_by": 311828434584080384, + "child_views": null, + "last_edited_by": 311828434584080384, + "last_edited_time": 1724149734 + }, + "child_views": [ + { + "icon": { + "ty": 0, + "value": "🏫" + }, + "name": "View of grid1", + "extra": null, + "layout": 1, + "view_id": "d8589e98-88fc-42e4-888c-b03338bf22bb", + "created_at": 1724146952, + "created_by": 311828434584080384, + "child_views": null, + "last_edited_by": 311828434584080384, + "last_edited_time": 1724149731 + } + ], + "ancestor_views": [ + { + "icon": null, + "name": "Workspace", + "extra": null, + "layout": 0, + "view_id": "043832c3-c9c4-40e8-ae02-e25677ef344f", + "created_at": 1724147455, + "created_by": 311828434584080384, + "child_views": null, + "last_edited_by": 311828434584080384, + "last_edited_time": 1724147455 + }, + { + "icon": null, + "name": "Shared", + "extra": "{\"is_space\":true,\"space_icon\":\"space_icon_1\",\"space_icon_color\":\"0xFFA34AFD\",\"space_permission\":0,\"space_created_at\":1724143277323}", + "layout": 0, + "view_id": "52adbe8e-57c1-43e9-8ef0-e7d49618aa27", + "created_at": 1724146934, + "created_by": 311828434584080384, + "child_views": null, + "last_edited_by": 311828434584080384, + "last_edited_time": 1724146934 + }, + { + "icon": { + "ty": 0, + "value": "🍽️" + }, + "name": "grid1", + "extra": null, + "layout": 1, + "view_id": "8e062f61-d7ae-4f4b-869c-f44c43149399", + "created_at": 1724146952, + "created_by": 311828434584080384, + "child_views": null, + "last_edited_by": 311828434584080384, + "last_edited_time": 1724149734 + } + ] +} +"#; + +pub const GRID_1_DB_DATA: &str = "7b2264617461626173655f636f6c6c6162223a5b32362c31372c3233312c3139322c3138392c3234312c31352c302c3136312c3235322c3231352c3137372c3136352c31332c312c312c33392c302c3235322c3231352c3137372c3136352c31332c332c33362c3130302c35362c35332c35362c35372c3130312c35372c35362c34352c35362c35362c3130322c39392c34352c35322c35302c3130312c35322c34352c35362c35362c35362c39392c34352c39382c34382c35312c35312c35312c35362c39382c3130322c35302c35302c39382c39382c312c34302c302c3233312c3139322c3138392c3234312c31352c312c322c3130352c3130302c312c3131392c33362c3130302c35362c35332c35362c35372c3130312c35372c35362c34352c35362c35362c3130322c39392c34352c35322c35302c3130312c35322c34352c35362c35362c35362c39392c34352c39382c34382c35312c35312c35312c35362c39382c3130322c35302c35302c39382c39382c34302c302c3233312c3139322c3138392c3234312c31352c312c31312c3130302c39372c3131362c39372c39382c39372c3131352c3130312c39352c3130352c3130302c312c3131392c33362c35332c39372c35362c34392c39382c35342c35332c39382c34352c3130322c39382c3130312c35362c34352c35322c35332c35322c3130322c34352c39382c35312c3130312c39392c34352c35352c35372c35332c39392c35342c3130322c34392c3130322c35342c35312c3130322c34392c34302c302c3233312c3139322c3138392c3234312c31352c312c342c3131302c39372c3130392c3130312c312c3131392c31332c38362c3130352c3130312c3131392c33322c3131312c3130322c33322c3130332c3131342c3130352c3130302c34392c34302c302c3233312c3139322c3138392c3234312c31352c312c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c302c34302c302c3233312c3139322c3138392c3234312c31352c312c31312c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c39352c39372c3131362c312c3132352c302c33392c302c3233312c3139322c3138392c3234312c31352c312c31352c3130382c39372c3132312c3131312c3131372c3131362c39352c3131352c3130312c3131362c3131362c3130352c3131302c3130332c3131352c312c34302c302c3233312c3139322c3138392c3234312c31352c312c362c3130382c39372c3132312c3131312c3131372c3131362c312c3132322c302c302c302c302c302c302c302c302c33392c302c3233312c3139322c3138392c3234312c31352c312c31342c3130322c3130352c3130312c3130382c3130302c39352c3131352c3130312c3131362c3131362c3130352c3131302c3130332c3131352c312c33392c302c3233312c3139322c3138392c3234312c31352c312c372c3130322c3130352c3130382c3131362c3130312c3131342c3131352c302c33392c302c3233312c3139322c3138392c3234312c31352c312c362c3130332c3131342c3131312c3131372c3131322c3131352c302c33392c302c3233312c3139322c3138392c3234312c31352c312c352c3131352c3131312c3131342c3131362c3131352c302c33392c302c3233312c3139322c3138392c3234312c31352c312c31322c3130322c3130352c3130312c3130382c3130302c39352c3131312c3131342c3130302c3130312c3131342c3131352c302c382c302c3233312c3139322c3138392c3234312c31352c31332c332c3131382c312c322c3130352c3130302c3131392c362c3131332c37302c3131332c3131302c38362c3130312c3131382c312c322c3130352c3130302c3131392c362c34392c37312c37362c37302c37312c3131332c3131382c312c322c3130352c3130302c3131392c362c3131352c36382c37342c3131362c3132312c37362c33392c302c3233312c3139322c3138392c3234312c31352c312c31302c3131342c3131312c3131392c39352c3131312c3131342c3130302c3130312c3131342c3131352c302c382c302c3233312c3139322c3138392c3234312c31352c31372c332c3131382c322c322c3130352c3130302c3131392c33362c3130312c35312c34392c35312c3130322c34382c34382c35372c34352c34392c39372c35342c39392c34352c35322c3130322c35352c35322c34352c39372c39392c39392c39382c34352c34392c35302c39382c3130312c35302c35342c35322c34392c39372c39372c35342c35312c362c3130342c3130312c3130352c3130332c3130342c3131362c3132352c36302c3131382c322c322c3130352c3130302c3131392c33362c35372c35362c35332c39382c35302c39392c35302c35322c34352c3130312c35362c35302c35342c34352c35322c39372c35352c35332c34352c39372c35362c34392c35302c34352c39382c3130312c34392c35372c35362c34392c35302c35372c34392c39372c39392c35312c362c3130342c3130312c3130352c3130332c3130342c3131362c3132352c36302c3131382c322c362c3130342c3130312c3130352c3130332c3130342c3131362c3132352c36302c322c3130352c3130302c3131392c33362c39382c39372c35322c3130312c3130302c34382c35362c39372c34352c35342c3130302c34382c35342c34352c35322c35302c34382c3130312c34352c35372c34382c39382c34392c34352c3130322c35372c35342c3130312c35342c35312c3130302c35322c35342c35322c3130312c35352c312c3133312c3233382c3136322c3139372c31352c302c3136312c3233322c3233322c3234342c3136322c31352c302c312c312c3233322c3233322c3234342c3136322c31352c302c3136312c3231352c3136312c3133362c3233322c31322c302c312c312c3233362c3235352c3137392c3135392c31342c302c3136382c3231312c3139392c3133312c3235302c31302c302c312c3131392c33362c35332c39372c35362c34392c39382c35342c35332c39382c34352c3130322c39382c3130312c35362c34352c35322c35332c35322c3130322c34352c39382c35312c3130312c39392c34352c35352c35372c35332c39392c35342c3130322c34392c3130322c35342c35312c3130322c34392c312c3135352c3232332c3235302c3134382c31342c302c3136312c3233312c3139322c3138392c3234312c31352c302c312c36392c3235322c3231352c3137372c3136352c31332c302c33392c312c342c3130302c39372c3131362c39372c382c3130302c39372c3131362c39372c39382c39372c3131352c3130312c312c33332c302c3235322c3231352c3137372c3136352c31332c302c322c3130352c3130302c312c33392c302c3235322c3231352c3137372c3136352c31332c302c362c3130322c3130352c3130312c3130382c3130302c3131352c312c33392c302c3235322c3231352c3137372c3136352c31332c302c352c3131382c3130352c3130312c3131392c3131352c312c33392c302c3235322c3231352c3137372c3136352c31332c302c352c3130392c3130312c3131362c39372c3131352c312c34302c302c3235322c3231352c3137372c3136352c31332c342c332c3130352c3130352c3130302c312c3131392c33362c35362c3130312c34382c35342c35302c3130322c35342c34392c34352c3130302c35352c39372c3130312c34352c35322c3130322c35322c39382c34352c35362c35342c35372c39392c34352c3130322c35322c35322c39392c35322c35312c34392c35322c35372c35312c35372c35372c33392c302c3235322c3231352c3137372c3136352c31332c322c362c3131332c37302c3131332c3131302c38362c3130312c312c34302c302c3235322c3231352c3137372c3136352c31332c362c322c3130352c3130302c312c3131392c362c3131332c37302c3131332c3131302c38362c3130312c34302c302c3235322c3231352c3137372c3136352c31332c362c342c3131302c39372c3130392c3130312c312c3131392c342c37382c39372c3130392c3130312c34302c302c3235322c3231352c3137372c3136352c31332c362c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3133312c3134362c3136332c3233362c31322c34302c302c3235322c3231352c3137372c3136352c31332c362c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c3132352c3133312c3134362c3136332c3233362c31322c34302c302c3235322c3231352c3137372c3136352c31332c362c31302c3130352c3131352c39352c3131322c3131342c3130352c3130392c39372c3131342c3132312c312c3132302c34302c302c3235322c3231352c3137372c3136352c31332c362c322c3131362c3132312c312c3132352c302c33392c302c3235322c3231352c3137372c3136352c31332c362c31312c3131362c3132312c3131322c3130312c39352c3131312c3131322c3131362c3130352c3131312c3131302c312c33392c302c3235322c3231352c3137372c3136352c31332c31332c312c34382c312c34302c302c3235322c3231352c3137372c3136352c31332c31342c342c3130302c39372c3131362c39372c312c3131392c302c33392c302c3235322c3231352c3137372c3136352c31332c322c362c34392c37312c37362c37302c37312c3131332c312c34302c302c3235322c3231352c3137372c3136352c31332c31362c322c3130352c3130302c312c3131392c362c34392c37312c37362c37302c37312c3131332c34302c302c3235322c3231352c3137372c3136352c31332c31362c342c3131302c39372c3130392c3130312c312c3131392c342c38342c3132312c3131322c3130312c34302c302c3235322c3231352c3137372c3136352c31332c31362c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3133312c3134362c3136332c3233362c31322c33332c302c3235322c3231352c3137372c3136352c31332c31362c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c34302c302c3235322c3231352c3137372c3136352c31332c31362c31302c3130352c3131352c39352c3131322c3131342c3130352c3130392c39372c3131342c3132312c312c3132312c34302c302c3235322c3231352c3137372c3136352c31332c31362c322c3131362c3132312c312c3132352c332c33392c302c3235322c3231352c3137372c3136352c31332c31362c31312c3131362c3132312c3131322c3130312c39352c3131312c3131322c3131362c3130352c3131312c3131302c312c33392c302c3235322c3231352c3137372c3136352c31332c32332c312c35312c312c33332c302c3235322c3231352c3137372c3136352c31332c32342c372c39392c3131312c3131302c3131362c3130312c3131302c3131362c312c33392c302c3235322c3231352c3137372c3136352c31332c322c362c3131352c36382c37342c3131362c3132312c37362c312c34302c302c3235322c3231352c3137372c3136352c31332c32362c322c3130352c3130302c312c3131392c362c3131352c36382c37342c3131362c3132312c37362c34302c302c3235322c3231352c3137372c3136352c31332c32362c342c3131302c39372c3130392c3130312c312c3131392c342c36382c3131312c3131302c3130312c34302c302c3235322c3231352c3137372c3136352c31332c32362c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3133312c3134362c3136332c3233362c31322c34302c302c3235322c3231352c3137372c3136352c31332c32362c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c3132352c3133312c3134362c3136332c3233362c31322c34302c302c3235322c3231352c3137372c3136352c31332c32362c31302c3130352c3131352c39352c3131322c3131342c3130352c3130392c39372c3131342c3132312c312c3132312c34302c302c3235322c3231352c3137372c3136352c31332c32362c322c3131362c3132312c312c3132352c352c33392c302c3235322c3231352c3137372c3136352c31332c32362c31312c3131362c3132312c3131322c3130312c39352c3131312c3131322c3131362c3130352c3131312c3131302c312c33392c302c3235322c3231352c3137372c3136352c31332c33332c312c35332c312c33392c302c3235322c3231352c3137372c3136352c31332c332c33362c35362c3130312c34382c35342c35302c3130322c35342c34392c34352c3130302c35352c39372c3130312c34352c35322c3130322c35322c39382c34352c35362c35342c35372c39392c34352c3130322c35322c35322c39392c35322c35312c34392c35322c35372c35312c35372c35372c312c34302c302c3235322c3231352c3137372c3136352c31332c33352c322c3130352c3130302c312c3131392c33362c35362c3130312c34382c35342c35302c3130322c35342c34392c34352c3130302c35352c39372c3130312c34352c35322c3130322c35322c39382c34352c35362c35342c35372c39392c34352c3130322c35322c35322c39392c35322c35312c34392c35322c35372c35312c35372c35372c34302c302c3235322c3231352c3137372c3136352c31332c33352c31312c3130302c39372c3131362c39372c39382c39372c3131352c3130312c39352c3130352c3130302c312c3131392c33362c35332c39372c35362c34392c39382c35342c35332c39382c34352c3130322c39382c3130312c35362c34352c35322c35332c35322c3130322c34352c39382c35312c3130312c39392c34352c35352c35372c35332c39392c35342c3130322c34392c3130322c35342c35312c3130322c34392c34302c302c3235322c3231352c3137372c3136352c31332c33352c342c3131302c39372c3130392c3130312c312c3131392c382c38352c3131302c3131362c3130352c3131362c3130382c3130312c3130302c34302c302c3235322c3231352c3137372c3136352c31332c33352c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3133312c3134362c3136332c3233362c31322c34302c302c3235322c3231352c3137372c3136352c31332c33352c31312c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c39352c39372c3131362c312c3132352c3133312c3134362c3136332c3233362c31322c33392c302c3235322c3231352c3137372c3136352c31332c33352c31352c3130382c39372c3132312c3131312c3131372c3131362c39352c3131352c3130312c3131362c3131362c3130352c3131302c3130332c3131352c312c34302c302c3235322c3231352c3137372c3136352c31332c33352c362c3130382c39372c3132312c3131312c3131372c3131362c312c3132322c302c302c302c302c302c302c302c302c33392c302c3235322c3231352c3137372c3136352c31332c33352c31342c3130322c3130352c3130312c3130382c3130302c39352c3131352c3130312c3131362c3131362c3130352c3131302c3130332c3131352c312c33392c302c3235322c3231352c3137372c3136352c31332c34332c362c3131332c37302c3131332c3131302c38362c3130312c312c34302c302c3235322c3231352c3137372c3136352c31332c34342c31302c3131382c3130352c3131352c3130352c39382c3130352c3130382c3130352c3131362c3132312c312c3132352c302c34302c302c3235322c3231352c3137372c3136352c31332c34342c352c3131392c3130352c3130302c3131362c3130342c312c3132352c3135302c322c34302c302c3235322c3231352c3137372c3136352c31332c34342c342c3131392c3131342c39372c3131322c312c3132302c33392c302c3235322c3231352c3137372c3136352c31332c34332c362c3131352c36382c37342c3131362c3132312c37362c312c34302c302c3235322c3231352c3137372c3136352c31332c34382c31302c3131382c3130352c3131352c3130352c39382c3130352c3130382c3130352c3131362c3132312c312c3132352c302c34302c302c3235322c3231352c3137372c3136352c31332c34382c342c3131392c3131342c39372c3131322c312c3132302c34302c302c3235322c3231352c3137372c3136352c31332c34382c352c3131392c3130352c3130302c3131362c3130342c312c3132352c3135302c322c33392c302c3235322c3231352c3137372c3136352c31332c34332c362c34392c37312c37362c37302c37312c3131332c312c34302c302c3235322c3231352c3137372c3136352c31332c35322c31302c3131382c3130352c3131352c3130352c39382c3130352c3130382c3130352c3131362c3132312c312c3132352c302c34302c302c3235322c3231352c3137372c3136352c31332c35322c342c3131392c3131342c39372c3131322c312c3132302c34302c302c3235322c3231352c3137372c3136352c31332c35322c352c3131392c3130352c3130302c3131362c3130342c312c3132352c3135302c322c33392c302c3235322c3231352c3137372c3136352c31332c33352c372c3130322c3130352c3130382c3131362c3130312c3131342c3131352c302c33392c302c3235322c3231352c3137372c3136352c31332c33352c362c3130332c3131342c3131312c3131372c3131322c3131352c302c33392c302c3235322c3231352c3137372c3136352c31332c33352c352c3131352c3131312c3131342c3131362c3131352c302c33392c302c3235322c3231352c3137372c3136352c31332c33352c31322c3130322c3130352c3130312c3130382c3130302c39352c3131312c3131342c3130302c3130312c3131342c3131352c302c382c302c3235322c3231352c3137372c3136352c31332c35392c332c3131382c312c322c3130352c3130302c3131392c362c3131332c37302c3131332c3131302c38362c3130312c3131382c312c322c3130352c3130302c3131392c362c34392c37312c37362c37302c37312c3131332c3131382c312c322c3130352c3130302c3131392c362c3131352c36382c37342c3131362c3132312c37362c33392c302c3235322c3231352c3137372c3136352c31332c33352c31302c3131342c3131312c3131392c39352c3131312c3131342c3130302c3130312c3131342c3131352c302c382c302c3235322c3231352c3137372c3136352c31332c36332c332c3131382c322c362c3130342c3130312c3130352c3130332c3130342c3131362c3132352c36302c322c3130352c3130302c3131392c33362c3130312c35312c34392c35312c3130322c34382c34382c35372c34352c34392c39372c35342c39392c34352c35322c3130322c35352c35322c34352c39372c39392c39392c39382c34352c34392c35302c39382c3130312c35302c35342c35322c34392c39372c39372c35342c35312c3131382c322c362c3130342c3130312c3130352c3130332c3130342c3131362c3132352c36302c322c3130352c3130302c3131392c33362c35372c35362c35332c39382c35302c39392c35302c35322c34352c3130312c35362c35302c35342c34352c35322c39372c35352c35332c34352c39372c35362c34392c35302c34352c39382c3130312c34392c35372c35362c34392c35302c35372c34392c39372c39392c35312c3131382c322c362c3130342c3130312c3130352c3130332c3130342c3131362c3132352c36302c322c3130352c3130302c3131392c33362c39382c39372c35322c3130312c3130302c34382c35362c39372c34352c35342c3130302c34382c35342c34352c35322c35302c34382c3130312c34352c35372c34382c39382c34392c34352c3130322c35372c35342c3130312c35342c35312c3130302c35322c35342c35322c3130312c35352c3136312c3235322c3231352c3137372c3136352c31332c32302c312c3136312c3235322c3231352c3137372c3136352c31332c32352c312c3136312c3235322c3231352c3137372c3136352c31332c36372c312c3136312c3235322c3231352c3137372c3136352c31332c36382c312c3136382c3235322c3231352c3137372c3136352c31332c36392c312c3132352c3134382c3134362c3136332c3233362c31322c3136382c3235322c3231352c3137372c3136352c31332c37302c312c3131392c3136322c312c3132332c33342c3131312c3131322c3131362c3130352c3131312c3131302c3131352c33342c35382c39312c3132332c33342c3130352c3130302c33342c35382c33342c35352c35342c39392c3130382c33342c34342c33342c3131302c39372c3130392c3130312c33342c35382c33342c39392c33342c34342c33342c39392c3131312c3130382c3131312c3131342c33342c35382c33342c37362c3130352c3130332c3130342c3131362c38302c3130352c3131302c3130372c33342c3132352c34342c3132332c33342c3130352c3130302c33342c35382c33342c3132312c3130312c3131302c34352c33342c34342c33342c3131302c39372c3130392c3130312c33342c35382c33342c3130382c33342c34342c33342c39392c3131312c3130382c3131312c3131342c33342c35382c33342c38302c3130352c3131302c3130372c33342c3132352c34342c3132332c33342c3130352c3130302c33342c35382c33342c38322c3131322c34382c35332c33342c34342c33342c3131302c39372c3130392c3130312c33342c35382c33342c3130342c33342c34342c33342c39392c3131312c3130382c3131312c3131342c33342c35382c33342c38302c3131372c3131342c3131322c3130382c3130312c33342c3132352c39332c34342c33342c3130302c3130352c3131352c39372c39382c3130382c3130312c39352c39392c3131312c3130382c3131312c3131342c33342c35382c3130322c39372c3130382c3131352c3130312c3132352c312c3234372c3234312c3233392c3233332c31322c302c3136312c3231352c3136312c3133362c3233322c31322c302c312c312c3231352c3136312c3133362c3233322c31322c302c3136312c3135352c3232332c3235302c3134382c31342c302c312c312c3136342c3235352c3133352c3135392c31322c302c3136312c3234372c3234312c3233392c3233332c31322c302c312c312c3136302c3136332c3230342c3230302c31312c302c3136312c3233312c3139322c3138392c3234312c31352c302c312c312c3231302c3133382c3230362c3133342c31312c302c3136312c3233312c3139322c3138392c3234312c31352c302c312c312c3231312c3139392c3133312c3235302c31302c302c3136312c3230322c3136332c3230382c3137302c392c302c312c312c3233352c3136332c3230382c3137302c392c302c3136312c3233322c3233322c3234342c3136322c31352c302c312c312c3230322c3136332c3230382c3137302c392c302c3136312c3234382c3137332c3230362c3230312c322c302c312c312c3233342c3139382c3234332c3138312c382c302c3136312c3234382c3137332c3230362c3230312c322c302c312c312c3137362c3234302c3233302c3137392c382c302c3136312c3135352c3232332c3235302c3134382c31342c302c312c312c3132382c3132392c3134392c3231352c372c302c3136312c3233322c3233322c3234342c3136322c31352c302c312c312c3234312c3134312c3232332c3137362c372c302c3136312c3230322c3136332c3230382c3137302c392c302c312c312c3133382c3233312c3232392c3232392c362c302c3136312c3133322c3232352c3134382c3135392c332c302c312c312c3230342c3231322c3235332c3230372c362c302c3136312c3233312c3139322c3138392c3234312c31352c302c312c312c3139352c3232392c3137372c3231322c352c302c3136312c3234382c3137332c3230362c3230312c322c302c312c312c3133322c3232352c3134382c3135392c332c302c3136312c3235322c3231352c3137372c3136352c31332c312c312c312c3232312c3134392c3135382c3232392c322c302c3136312c3137362c3234302c3233302c3137392c382c302c312c312c3234382c3137332c3230362c3230312c322c302c3136312c3133312c3233382c3136322c3139372c31352c302c312c312c3136362c3230342c3135322c35372c302c3136312c3233322c3233322c3234342c3136322c31352c302c312c322c3132392c3233382c3134322c34392c302c33332c312c342c3130392c3130312c3131362c39372c31322c3130382c39372c3131352c3131362c39352c3131352c3132312c3131302c39392c39352c39372c3131362c3136392c312c3136382c3132392c3233382c3134322c34392c3136382c312c312c3132352c3136392c3139312c3136332c3233362c31322c32352c3136302c3136332c3230342c3230302c31312c312c302c312c3132392c3233382c3134322c34392c312c302c3136392c312c3132382c3132392c3134392c3231352c372c312c302c312c3133312c3233382c3136322c3139372c31352c312c302c312c3133322c3232352c3134382c3135392c332c312c302c312c3136342c3235352c3133352c3135392c31322c312c302c312c3136362c3230342c3135322c35372c312c302c312c3233312c3139322c3138392c3234312c31352c312c302c312c3233322c3233322c3234342c3136322c31352c312c302c312c3139352c3232392c3137372c3231322c352c312c302c312c3133382c3233312c3232392c3232392c362c312c302c312c3233352c3136332c3230382c3137302c392c312c302c312c3230342c3231322c3235332c3230372c362c312c302c312c3230322c3136332c3230382c3137302c392c312c302c312c3233342c3139382c3234332c3138312c382c312c302c312c3137362c3234302c3233302c3137392c382c312c302c312c3234312c3134312c3232332c3137362c372c312c302c312c3231302c3133382c3230362c3133342c31312c312c302c312c3231312c3139392c3133312c3235302c31302c312c302c312c3231352c3136312c3133362c3233322c31322c312c302c312c3234372c3234312c3233392c3233332c31322c312c302c312c3234382c3137332c3230362c3230312c322c312c302c312c3135352c3232332c3235302c3134382c31342c312c302c312c3235322c3231352c3137372c3136352c31332c342c312c312c32302c312c32352c312c36372c342c3232312c3134392c3135382c3232392c322c312c302c315d2c2264617461626173655f726f775f636f6c6c616273223a7b2239383562326332342d653832362d346137352d613831322d626531393831323931616333223a5b322c322c3134302c3138302c3232362c3232352c382c302c33332c312c342c3130392c3130312c3131362c39372c31322c3130382c39372c3131352c3131362c39352c3131352c3132312c3131302c39392c39352c39372c3131362c37302c3136382c3134302c3138302c3232362c3232352c382c36392c312c3132352c3136352c3139312c3136332c3233362c31322c32382c3232312c3134382c3233312c3139322c352c302c33392c312c342c3130302c39372c3131362c39372c342c3130302c39372c3131362c39372c312c33392c312c342c3130302c39372c3131362c39372c342c3130392c3130312c3131362c39372c312c33392c312c342c3130302c39372c3131362c39372c372c39392c3131312c3130392c3130392c3130312c3131302c3131362c302c34302c302c3232312c3134382c3233312c3139322c352c302c322c3130352c3130302c312c3131392c33362c35372c35362c35332c39382c35302c39392c35302c35322c34352c3130312c35362c35302c35342c34352c35322c39372c35352c35332c34352c39372c35362c34392c35302c34352c39382c3130312c34392c35372c35362c34392c35302c35372c34392c39372c39392c35312c34302c302c3232312c3134382c3233312c3139322c352c302c31312c3130302c39372c3131362c39372c39382c39372c3131352c3130312c39352c3130352c3130302c312c3131392c33362c35332c39372c35362c34392c39382c35342c35332c39382c34352c3130322c39382c3130312c35362c34352c35322c35332c35322c3130322c34352c39382c35312c3130312c39392c34352c35352c35372c35332c39392c35342c3130322c34392c3130322c35342c35312c3130322c34392c34302c302c3232312c3134382c3233312c3139322c352c302c362c3130342c3130312c3130352c3130332c3130342c3131362c312c3132352c36302c34302c302c3232312c3134382c3233312c3139322c352c302c31302c3131382c3130352c3131352c3130352c39382c3130352c3130382c3130352c3131362c3132312c312c3132302c34302c302c3232312c3134382c3233312c3139322c352c302c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3133312c3134362c3136332c3233362c31322c33332c302c3232312c3134382c3233312c3139322c352c302c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c33392c302c3232312c3134382c3233312c3139322c352c302c352c39392c3130312c3130382c3130382c3131352c312c3136312c3232312c3134382c3233312c3139322c352c382c312c33392c302c3232312c3134382c3233312c3139322c352c392c362c3131332c37302c3131332c3131302c38362c3130312c312c34302c302c3232312c3134382c3233312c3139322c352c31312c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3134342c3134362c3136332c3233362c31322c34302c302c3232312c3134382c3233312c3139322c352c31312c31302c3130322c3130352c3130312c3130382c3130302c39352c3131362c3132312c3131322c3130312c312c3132352c302c34302c302c3232312c3134382c3233312c3139322c352c31312c342c3130302c39372c3131362c39372c312c3131392c312c3130382c34302c302c3232312c3134382c3233312c3139322c352c31312c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c3132352c3134342c3134362c3136332c3233362c31322c3136312c3232312c3134382c3233312c3139322c352c31302c312c33392c302c3232312c3134382c3233312c3139322c352c392c362c34392c37312c37362c37302c37312c3131332c312c34302c302c3232312c3134382c3233312c3139322c352c31372c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3134372c3134362c3136332c3233362c31322c34302c302c3232312c3134382c3233312c3139322c352c31372c342c3130302c39372c3131362c39372c312c3131392c342c3132312c3130312c3131302c34352c34302c302c3232312c3134382c3233312c3139322c352c31372c31302c3130322c3130352c3130312c3130382c3130302c39352c3131362c3132312c3131322c3130312c312c3132352c332c34302c302c3232312c3134382c3233312c3139322c352c31372c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c3132352c3134372c3134362c3136332c3233362c31322c3136382c3232312c3134382c3233312c3139322c352c31362c312c3132352c3135302c3134362c3136332c3233362c31322c33392c302c3232312c3134382c3233312c3139322c352c392c362c3131352c36382c37342c3131362c3132312c37362c312c34302c302c3232312c3134382c3233312c3139322c352c32332c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3135302c3134362c3136332c3233362c31322c34302c302c3232312c3134382c3233312c3139322c352c32332c31302c3130322c3130352c3130312c3130382c3130302c39352c3131362c3132312c3131322c3130312c312c3132352c352c34302c302c3232312c3134382c3233312c3139322c352c32332c342c3130302c39372c3131362c39372c312c3131392c332c38392c3130312c3131352c34302c302c3232312c3134382c3233312c3139322c352c32332c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c3132352c3135302c3134362c3136332c3233362c31322c322c3134302c3138302c3232362c3232352c382c312c302c37302c3232312c3134382c3233312c3139322c352c332c382c312c31302c312c31362c315d2c2265333133663030392d316136632d346637342d616363622d313262653236343161613633223a5b322c32382c3134342c3139342c3136382c3230352c31322c302c33392c312c342c3130302c39372c3131362c39372c342c3130302c39372c3131362c39372c312c33392c312c342c3130302c39372c3131362c39372c342c3130392c3130312c3131362c39372c312c33392c312c342c3130302c39372c3131362c39372c372c39392c3131312c3130392c3130392c3130312c3131302c3131362c302c34302c302c3134342c3139342c3136382c3230352c31322c302c322c3130352c3130302c312c3131392c33362c3130312c35312c34392c35312c3130322c34382c34382c35372c34352c34392c39372c35342c39392c34352c35322c3130322c35352c35322c34352c39372c39392c39392c39382c34352c34392c35302c39382c3130312c35302c35342c35322c34392c39372c39372c35342c35312c34302c302c3134342c3139342c3136382c3230352c31322c302c31312c3130302c39372c3131362c39372c39382c39372c3131352c3130312c39352c3130352c3130302c312c3131392c33362c35332c39372c35362c34392c39382c35342c35332c39382c34352c3130322c39382c3130312c35362c34352c35322c35332c35322c3130322c34352c39382c35312c3130312c39392c34352c35352c35372c35332c39392c35342c3130322c34392c3130322c35342c35312c3130322c34392c34302c302c3134342c3139342c3136382c3230352c31322c302c362c3130342c3130312c3130352c3130332c3130342c3131362c312c3132352c36302c34302c302c3134342c3139342c3136382c3230352c31322c302c31302c3131382c3130352c3131352c3130352c39382c3130352c3130382c3130352c3131362c3132312c312c3132302c34302c302c3134342c3139342c3136382c3230352c31322c302c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3133312c3134362c3136332c3233362c31322c33332c302c3134342c3139342c3136382c3230352c31322c302c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c33392c302c3134342c3139342c3136382c3230352c31322c302c352c39392c3130312c3130382c3130382c3131352c312c3136312c3134342c3139342c3136382c3230352c31322c382c312c33392c302c3134342c3139342c3136382c3230352c31322c392c362c3131332c37302c3131332c3131302c38362c3130312c312c34302c302c3134342c3139342c3136382c3230352c31322c31312c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3134342c3134362c3136332c3233362c31322c34302c302c3134342c3139342c3136382c3230352c31322c31312c342c3130302c39372c3131362c39372c312c3131392c312c3130342c34302c302c3134342c3139342c3136382c3230352c31322c31312c31302c3130322c3130352c3130312c3130382c3130302c39352c3131362c3132312c3131322c3130312c312c3132352c302c34302c302c3134342c3139342c3136382c3230352c31322c31312c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c3132352c3134342c3134362c3136332c3233362c31322c3136312c3134342c3139342c3136382c3230352c31322c31302c312c33392c302c3134342c3139342c3136382c3230352c31322c392c362c34392c37312c37362c37302c37312c3131332c312c34302c302c3134342c3139342c3136382c3230352c31322c31372c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3134362c3134362c3136332c3233362c31322c34302c302c3134342c3139342c3136382c3230352c31322c31372c31302c3130322c3130352c3130312c3130382c3130302c39352c3131362c3132312c3131322c3130312c312c3132352c332c34302c302c3134342c3139342c3136382c3230352c31322c31372c342c3130302c39372c3131362c39372c312c3131392c342c38322c3131322c34382c35332c34302c302c3134342c3139342c3136382c3230352c31322c31372c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c3132352c3134362c3134362c3136332c3233362c31322c3136382c3134342c3139342c3136382c3230352c31322c31362c312c3132352c3135302c3134362c3136332c3233362c31322c33392c302c3134342c3139342c3136382c3230352c31322c392c362c3131352c36382c37342c3131362c3132312c37362c312c34302c302c3134342c3139342c3136382c3230352c31322c32332c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3135302c3134362c3136332c3233362c31322c34302c302c3134342c3139342c3136382c3230352c31322c32332c31302c3130322c3130352c3130312c3130382c3130302c39352c3131362c3132312c3131322c3130312c312c3132352c352c34302c302c3134342c3139342c3136382c3230352c31322c32332c342c3130302c39372c3131362c39372c312c3131392c332c38392c3130312c3131352c34302c302c3134342c3139342c3136382c3230352c31322c32332c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c3132352c3135302c3134362c3136332c3233362c31322c322c3233352c3135362c3137352c3234362c322c302c33332c312c342c3130392c3130312c3131362c39372c31322c3130382c39372c3131352c3131362c39352c3131352c3132312c3131302c39392c39352c39372c3131362c36382c3136382c3233352c3135362c3137352c3234362c322c36372c312c3132352c3136352c3139312c3136332c3233362c31322c322c3134342c3139342c3136382c3230352c31322c332c382c312c31302c312c31362c312c3233352c3135362c3137352c3234362c322c312c302c36385d2c2262613465643038612d366430362d343230652d393062312d663936653633643436346537223a5b322c322c3231352c3132382c3138392c3133302c322c302c33332c312c342c3130392c3130312c3131362c39372c31322c3130382c39372c3131352c3131362c39352c3131352c3132312c3131302c39392c39352c39372c3131362c36362c3136382c3231352c3132382c3138392c3133302c322c36352c312c3132352c3136352c3139312c3136332c3233362c31322c32382c3135322c3133382c3233302c36302c302c33392c312c342c3130302c39372c3131362c39372c342c3130302c39372c3131362c39372c312c33392c312c342c3130302c39372c3131362c39372c342c3130392c3130312c3131362c39372c312c33392c312c342c3130302c39372c3131362c39372c372c39392c3131312c3130392c3130392c3130312c3131302c3131362c302c34302c302c3135322c3133382c3233302c36302c302c322c3130352c3130302c312c3131392c33362c39382c39372c35322c3130312c3130302c34382c35362c39372c34352c35342c3130302c34382c35342c34352c35322c35302c34382c3130312c34352c35372c34382c39382c34392c34352c3130322c35372c35342c3130312c35342c35312c3130302c35322c35342c35322c3130312c35352c34302c302c3135322c3133382c3233302c36302c302c31312c3130302c39372c3131362c39372c39382c39372c3131352c3130312c39352c3130352c3130302c312c3131392c33362c35332c39372c35362c34392c39382c35342c35332c39382c34352c3130322c39382c3130312c35362c34352c35322c35332c35322c3130322c34352c39382c35312c3130312c39392c34352c35352c35372c35332c39392c35342c3130322c34392c3130322c35342c35312c3130322c34392c34302c302c3135322c3133382c3233302c36302c302c362c3130342c3130312c3130352c3130332c3130342c3131362c312c3132352c36302c34302c302c3135322c3133382c3233302c36302c302c31302c3131382c3130352c3131352c3130352c39382c3130352c3130382c3130352c3131362c3132312c312c3132302c34302c302c3135322c3133382c3233302c36302c302c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3133312c3134362c3136332c3233362c31322c33332c302c3135322c3133382c3233302c36302c302c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c33392c302c3135322c3133382c3233302c36302c302c352c39392c3130312c3130382c3130382c3131352c312c3136312c3135322c3133382c3233302c36302c382c312c33392c302c3135322c3133382c3233302c36302c392c362c3131332c37302c3131332c3131302c38362c3130312c312c34302c302c3135322c3133382c3233302c36302c31312c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3134352c3134362c3136332c3233362c31322c34302c302c3135322c3133382c3233302c36302c31312c342c3130302c39372c3131362c39372c312c3131392c312c39392c34302c302c3135322c3133382c3233302c36302c31312c31302c3130322c3130352c3130312c3130382c3130302c39352c3131362c3132312c3131322c3130312c312c3132352c302c34302c302c3135322c3133382c3233302c36302c31312c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c3132352c3134352c3134362c3136332c3233362c31322c3136312c3135322c3133382c3233302c36302c31302c312c33392c302c3135322c3133382c3233302c36302c392c362c34392c37312c37362c37302c37312c3131332c312c34302c302c3135322c3133382c3233302c36302c31372c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3134382c3134362c3136332c3233362c31322c34302c302c3135322c3133382c3233302c36302c31372c342c3130302c39372c3131362c39372c312c3131392c342c35352c35342c39392c3130382c34302c302c3135322c3133382c3233302c36302c31372c31302c3130322c3130352c3130312c3130382c3130302c39352c3131362c3132312c3131322c3130312c312c3132352c332c34302c302c3135322c3133382c3233302c36302c31372c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c3132352c3134382c3134362c3136332c3233362c31322c3136382c3135322c3133382c3233302c36302c31362c312c3132352c3134392c3134362c3136332c3233362c31322c33392c302c3135322c3133382c3233302c36302c392c362c3131352c36382c37342c3131362c3132312c37362c312c34302c302c3135322c3133382c3233302c36302c32332c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3134392c3134362c3136332c3233362c31322c34302c302c3135322c3133382c3233302c36302c32332c342c3130302c39372c3131362c39372c312c3131392c332c38392c3130312c3131352c34302c302c3135322c3133382c3233302c36302c32332c31302c3130322c3130352c3130312c3130382c3130302c39352c3131362c3132312c3131322c3130312c312c3132352c352c34302c302c3135322c3133382c3233302c36302c32332c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c3132352c3134392c3134362c3136332c3233362c31322c322c3135322c3133382c3233302c36302c332c382c312c31302c312c31362c312c3231352c3132382c3138392c3133302c322c312c302c36365d7d2c2264617461626173655f726f775f646f63756d656e745f636f6c6c616273223a7b7d2c2276697369626c655f64617461626173655f766965775f696473223a5b2238653036326636312d643761652d346634622d383639632d663434633433313439333939222c2264383538396539382d383866632d343265342d383838632d623033333338626632326262225d2c2264617461626173655f72656c6174696f6e73223a7b2235613831623635622d666265382d343534662d623365632d373935633666316636336631223a2238653036326636312d643761652d346634622d383639632d663434633433313439333939227d7d"; + +pub const DOC_3_META: &str = r#" +{ + "view": { + "icon": { + "ty": 0, + "value": "🇧🇸" + }, + "name": "doc3", + "extra": null, + "layout": 0, + "view_id": "cc3c9716-914b-4e2c-860a-506c42bff8f8", + "created_at": 1724146934, + "created_by": 311828434584080384, + "child_views": null, + "last_edited_by": 311828434584080384, + "last_edited_time": 1724866836 + }, + "child_views": [], + "ancestor_views": [ + { + "icon": null, + "name": "Workspace", + "extra": null, + "layout": 0, + "view_id": "043832c3-c9c4-40e8-ae02-e25677ef344f", + "created_at": 1724147455, + "created_by": 311828434584080384, + "child_views": null, + "last_edited_by": 311828434584080384, + "last_edited_time": 1724147455 + }, + { + "icon": null, + "name": "Shared", + "extra": "{\"is_space\":true,\"space_icon\":\"space_icon_1\",\"space_icon_color\":\"0xFFA34AFD\",\"space_permission\":0,\"space_created_at\":1724143277323}", + "layout": 0, + "view_id": "52adbe8e-57c1-43e9-8ef0-e7d49618aa27", + "created_at": 1724319112, + "created_by": 311828434584080384, + "child_views": null, + "last_edited_by": 311828434584080384, + "last_edited_time": 1724319112 + }, + { + "icon": { + "ty": 0, + "value": "🇧🇸" + }, + "name": "doc3", + "extra": null, + "layout": 0, + "view_id": "cc3c9716-914b-4e2c-860a-506c42bff8f8", + "created_at": 1724146934, + "created_by": 311828434584080384, + "child_views": null, + "last_edited_by": 311828434584080384, + "last_edited_time": 1724866836 + } + ] +} +"#; + +pub const DOC_3_DOC_STATE_HEX: &str = "090bf09cfed80b00000b2700c587a7ac0b01063362346f6877012800f09cfed80b0b0269640177063362346f68772800f09cfed80b0b027479017704677269642800f09cfed80b0b06706172656e7401772437653439303839652d656337322d353736632d386235302d6635373166343731326636612800f09cfed80b0b086368696c6472656e017706776949302d582800f09cfed80b0b04646174610177657b22706172656e745f6964223a2238653036326636312d643761652d346634622d383639632d663434633433313439333939222c22766965775f6964223a2264383538396539382d383866632d343265342d383838632d623033333338626632326262227d2800f09cfed80b0b0b65787465726e616c5f6964017e2800f09cfed80b0b0d65787465726e616c5f74797065017e2700c587a7ac0b0306776949302d580048c587a7ac0b180177063362346f687714c587a7ac0b002701046461746108646f63756d656e74012700c587a7ac0b0006626c6f636b73012700c587a7ac0b00046d657461012700c587a7ac0b020c6368696c6472656e5f6d6170012700c587a7ac0b0208746578745f6d6170012800c587a7ac0b0007706167655f696401772437653439303839652d656337322d353736632d386235302d6635373166343731326636612700c587a7ac0b012437653439303839652d656337322d353736632d386235302d663537316634373132663661012800c587a7ac0b0602696401772437653439303839652d656337322d353736632d386235302d6635373166343731326636612800c587a7ac0b06027479017704706167652800c587a7ac0b0606706172656e740177002800c587a7ac0b06086368696c6472656e01772437653439303839652d656337322d353736632d386235302d6635373166343731326636612800c587a7ac0b0604646174610177027b7d2800c587a7ac0b060b65787465726e616c5f6964017e2800c587a7ac0b060d65787465726e616c5f74797065017e2700c587a7ac0b032437653439303839652d656337322d353736632d386235302d663537316634373132663661002100c587a7ac0b010a68516e7a534d486767680100072100c587a7ac0b030a564b7a31425a61377049010100c587a7ac0b0e012100c587a7ac0b040a6e7a41643871564546620101e4c1b8aa0b002101046d6574610c6c6173745f73796e635f61745802afd8ead10600a1d3bffa590907a8afd8ead10606017a0000000066c7034215caa3c68e060000052100c587a7ac0b0106466f7a4158680100072100c587a7ac0b0306672d4134557101c1f09cfed80b14c587a7ac0b18012100c587a7ac0b0406754532536b79012100c587a7ac0b01065675347662450100072100c587a7ac0b03066c67516b51620181c587a7ac0b180100012100c587a7ac0b04064c4b4e5f7a52012100c587a7ac0b01066537394c79760100072100c587a7ac0b0306594b36664d530181caa3c68e0619012100c587a7ac0b0406536536695444012100c587a7ac0b01066f726130306c0100072100c587a7ac0b03063956574332480181caa3c68e0625010fa7b9e38804002100c587a7ac0b0406726e48724865012100c587a7ac0b010657764f6836450100072100c587a7ac0b03065263436c43460181caa3c68e06300100042100c587a7ac0b010674334c4d38500100072100c587a7ac0b03064e37326c7a5101c1caa3c68e0630a7b9e388040a012100c587a7ac0b0406387472776864012100c587a7ac0b01065234487434300100072100c587a7ac0b0306794b4255576e0181a7b9e388040a0101f5d2b382030000010bf1c6a19401002700c587a7ac0b0406423558507539022700c587a7ac0b010631506b566245012800f1c6a194010102696401770631506b5662452800f1c6a19401010274790177097061726167726170682800f1c6a194010106706172656e7401772437653439303839652d656337322d353736632d386235302d6635373166343731326636612800f1c6a1940101086368696c6472656e0177064c4d68736f412800f1c6a194010104646174610177027b7d2800f1c6a19401010b65787465726e616c5f69640177064235585075392800f1c6a19401010d65787465726e616c5f74797065017704746578742700c587a7ac0b03064c4d68736f410088a7b9e388042301770631506b56624502d3bffa59002101046d6574610c6c6173745f73796e635f617401a1e4c1b8aa0b570908f09cfed80b01000bd3bffa5901000ae4c1b8aa0b010058c587a7ac0b010f0bf5d2b38203010001a7b9e38804010024caa3c68e06010031afd8ead106010007"; + +pub const VIEW_OF_GRID1_META: &str = r#" +{ + "view": { + "icon": { + "ty": 0, + "value": "🏫" + }, + "name": "View of grid1", + "extra": null, + "layout": 1, + "view_id": "d8589e98-88fc-42e4-888c-b03338bf22bb", + "created_at": 1724146952, + "created_by": 311828434584080384, + "child_views": null, + "last_edited_by": 311828434584080384, + "last_edited_time": 1724867361 + }, + "child_views": [], + "ancestor_views": [ + { + "icon": null, + "name": "Workspace", + "extra": null, + "layout": 0, + "view_id": "043832c3-c9c4-40e8-ae02-e25677ef344f", + "created_at": 1724147455, + "created_by": 311828434584080384, + "child_views": null, + "last_edited_by": 311828434584080384, + "last_edited_time": 1724147455 + }, + { + "icon": null, + "name": "Shared", + "extra": "{\"is_space\":true,\"space_icon\":\"space_icon_1\",\"space_icon_color\":\"0xFFA34AFD\",\"space_permission\":0,\"space_created_at\":1724143277323}", + "layout": 0, + "view_id": "52adbe8e-57c1-43e9-8ef0-e7d49618aa27", + "created_at": 1724319112, + "created_by": 311828434584080384, + "child_views": null, + "last_edited_by": 311828434584080384, + "last_edited_time": 1724319112 + }, + { + "icon": { + "ty": 0, + "value": "🍽️ " + }, + "name": "grid1", + "extra": null, + "layout": 1, + "view_id": "8e062f61-d7ae-4f4b-869c-f44c43149399", + "created_at": 1724146952, + "created_by": 311828434584080384, + "child_views": null, + "last_edited_by": 311828434584080384, + "last_edited_time": 1724866836 + }, + { + "icon": { + "ty": 0, + "value": "🏫" + }, + "name": "View of grid1", + "extra": null, + "layout": 1, + "view_id": "d8589e98-88fc-42e4-888c-b03338bf22bb", + "created_at": 1724146952, + "created_by": 311828434584080384, + "child_views": null, + "last_edited_by": 311828434584080384, + "last_edited_time": 1724867361 + } + ] +} +"#; + +pub const VIEW_OF_GRID_1_DB_DATA: &str = "7b2264617461626173655f636f6c6c6162223a5b34392c31372c3233312c3139322c3138392c3234312c31352c302c3136312c3235322c3231352c3137372c3136352c31332c312c312c33392c302c3235322c3231352c3137372c3136352c31332c332c33362c3130302c35362c35332c35362c35372c3130312c35372c35362c34352c35362c35362c3130322c39392c34352c35322c35302c3130312c35322c34352c35362c35362c35362c39392c34352c39382c34382c35312c35312c35312c35362c39382c3130322c35302c35302c39382c39382c312c34302c302c3233312c3139322c3138392c3234312c31352c312c322c3130352c3130302c312c3131392c33362c3130302c35362c35332c35362c35372c3130312c35372c35362c34352c35362c35362c3130322c39392c34352c35322c35302c3130312c35322c34352c35362c35362c35362c39392c34352c39382c34382c35312c35312c35312c35362c39382c3130322c35302c35302c39382c39382c34302c302c3233312c3139322c3138392c3234312c31352c312c31312c3130302c39372c3131362c39372c39382c39372c3131352c3130312c39352c3130352c3130302c312c3131392c33362c35332c39372c35362c34392c39382c35342c35332c39382c34352c3130322c39382c3130312c35362c34352c35322c35332c35322c3130322c34352c39382c35312c3130312c39392c34352c35352c35372c35332c39392c35342c3130322c34392c3130322c35342c35312c3130322c34392c34302c302c3233312c3139322c3138392c3234312c31352c312c342c3131302c39372c3130392c3130312c312c3131392c31332c38362c3130352c3130312c3131392c33322c3131312c3130322c33322c3130332c3131342c3130352c3130302c34392c34302c302c3233312c3139322c3138392c3234312c31352c312c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c302c34302c302c3233312c3139322c3138392c3234312c31352c312c31312c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c39352c39372c3131362c312c3132352c302c33392c302c3233312c3139322c3138392c3234312c31352c312c31352c3130382c39372c3132312c3131312c3131372c3131362c39352c3131352c3130312c3131362c3131362c3130352c3131302c3130332c3131352c312c34302c302c3233312c3139322c3138392c3234312c31352c312c362c3130382c39372c3132312c3131312c3131372c3131362c312c3132322c302c302c302c302c302c302c302c302c33392c302c3233312c3139322c3138392c3234312c31352c312c31342c3130322c3130352c3130312c3130382c3130302c39352c3131352c3130312c3131362c3131362c3130352c3131302c3130332c3131352c312c33392c302c3233312c3139322c3138392c3234312c31352c312c372c3130322c3130352c3130382c3131362c3130312c3131342c3131352c302c33392c302c3233312c3139322c3138392c3234312c31352c312c362c3130332c3131342c3131312c3131372c3131322c3131352c302c33392c302c3233312c3139322c3138392c3234312c31352c312c352c3131352c3131312c3131342c3131362c3131352c302c33392c302c3233312c3139322c3138392c3234312c31352c312c31322c3130322c3130352c3130312c3130382c3130302c39352c3131312c3131342c3130302c3130312c3131342c3131352c302c382c302c3233312c3139322c3138392c3234312c31352c31332c332c3131382c312c322c3130352c3130302c3131392c362c3131332c37302c3131332c3131302c38362c3130312c3131382c312c322c3130352c3130302c3131392c362c34392c37312c37362c37302c37312c3131332c3131382c312c322c3130352c3130302c3131392c362c3131352c36382c37342c3131362c3132312c37362c33392c302c3233312c3139322c3138392c3234312c31352c312c31302c3131342c3131312c3131392c39352c3131312c3131342c3130302c3130312c3131342c3131352c302c382c302c3233312c3139322c3138392c3234312c31352c31372c332c3131382c322c362c3130342c3130312c3130352c3130332c3130342c3131362c3132352c36302c322c3130352c3130302c3131392c33362c3130312c35312c34392c35312c3130322c34382c34382c35372c34352c34392c39372c35342c39392c34352c35322c3130322c35352c35322c34352c39372c39392c39392c39382c34352c34392c35302c39382c3130312c35302c35342c35322c34392c39372c39372c35342c35312c3131382c322c322c3130352c3130302c3131392c33362c35372c35362c35332c39382c35302c39392c35302c35322c34352c3130312c35362c35302c35342c34352c35322c39372c35352c35332c34352c39372c35362c34392c35302c34352c39382c3130312c34392c35372c35362c34392c35302c35372c34392c39372c39392c35312c362c3130342c3130312c3130352c3130332c3130342c3131362c3132352c36302c3131382c322c362c3130342c3130312c3130352c3130332c3130342c3131362c3132352c36302c322c3130352c3130302c3131392c33362c39382c39372c35322c3130312c3130302c34382c35362c39372c34352c35342c3130302c34382c35342c34352c35322c35302c34382c3130312c34352c35372c34382c39382c34392c34352c3130322c35372c35342c3130312c35342c35312c3130302c35322c35342c35322c3130312c35352c312c3133312c3233382c3136322c3139372c31352c302c3136312c3233322c3233322c3234342c3136322c31352c302c312c312c3233322c3233322c3234342c3136322c31352c302c3136312c3231352c3136312c3133362c3233322c31322c302c312c312c3231352c3233392c3138352c3138392c31342c302c3136312c3231352c3233302c3134382c3136332c342c302c312c312c3233362c3235352c3137392c3135392c31342c302c3136312c3231312c3139392c3133312c3235302c31302c302c312c312c3135352c3232332c3235302c3134382c31342c302c3136312c3233312c3139322c3138392c3234312c31352c302c312c312c3133312c3231382c3137312c3230372c31332c302c3136312c3135312c3232342c3134332c3137312c392c302c312c312c3136372c3133392c3234382c3139372c31332c302c3136312c3134362c3133382c3231332c3234342c362c302c312c36392c3235322c3231352c3137372c3136352c31332c302c33392c312c342c3130302c39372c3131362c39372c382c3130302c39372c3131362c39372c39382c39372c3131352c3130312c312c33332c302c3235322c3231352c3137372c3136352c31332c302c322c3130352c3130302c312c33392c302c3235322c3231352c3137372c3136352c31332c302c362c3130322c3130352c3130312c3130382c3130302c3131352c312c33392c302c3235322c3231352c3137372c3136352c31332c302c352c3131382c3130352c3130312c3131392c3131352c312c33392c302c3235322c3231352c3137372c3136352c31332c302c352c3130392c3130312c3131362c39372c3131352c312c34302c302c3235322c3231352c3137372c3136352c31332c342c332c3130352c3130352c3130302c312c3131392c33362c35362c3130312c34382c35342c35302c3130322c35342c34392c34352c3130302c35352c39372c3130312c34352c35322c3130322c35322c39382c34352c35362c35342c35372c39392c34352c3130322c35322c35322c39392c35322c35312c34392c35322c35372c35312c35372c35372c33392c302c3235322c3231352c3137372c3136352c31332c322c362c3131332c37302c3131332c3131302c38362c3130312c312c34302c302c3235322c3231352c3137372c3136352c31332c362c322c3130352c3130302c312c3131392c362c3131332c37302c3131332c3131302c38362c3130312c34302c302c3235322c3231352c3137372c3136352c31332c362c342c3131302c39372c3130392c3130312c312c3131392c342c37382c39372c3130392c3130312c34302c302c3235322c3231352c3137372c3136352c31332c362c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3133312c3134362c3136332c3233362c31322c34302c302c3235322c3231352c3137372c3136352c31332c362c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c3132352c3133312c3134362c3136332c3233362c31322c34302c302c3235322c3231352c3137372c3136352c31332c362c31302c3130352c3131352c39352c3131322c3131342c3130352c3130392c39372c3131342c3132312c312c3132302c34302c302c3235322c3231352c3137372c3136352c31332c362c322c3131362c3132312c312c3132352c302c33392c302c3235322c3231352c3137372c3136352c31332c362c31312c3131362c3132312c3131322c3130312c39352c3131312c3131322c3131362c3130352c3131312c3131302c312c33392c302c3235322c3231352c3137372c3136352c31332c31332c312c34382c312c34302c302c3235322c3231352c3137372c3136352c31332c31342c342c3130302c39372c3131362c39372c312c3131392c302c33392c302c3235322c3231352c3137372c3136352c31332c322c362c34392c37312c37362c37302c37312c3131332c312c34302c302c3235322c3231352c3137372c3136352c31332c31362c322c3130352c3130302c312c3131392c362c34392c37312c37362c37302c37312c3131332c34302c302c3235322c3231352c3137372c3136352c31332c31362c342c3131302c39372c3130392c3130312c312c3131392c342c38342c3132312c3131322c3130312c34302c302c3235322c3231352c3137372c3136352c31332c31362c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3133312c3134362c3136332c3233362c31322c33332c302c3235322c3231352c3137372c3136352c31332c31362c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c34302c302c3235322c3231352c3137372c3136352c31332c31362c31302c3130352c3131352c39352c3131322c3131342c3130352c3130392c39372c3131342c3132312c312c3132312c34302c302c3235322c3231352c3137372c3136352c31332c31362c322c3131362c3132312c312c3132352c332c33392c302c3235322c3231352c3137372c3136352c31332c31362c31312c3131362c3132312c3131322c3130312c39352c3131312c3131322c3131362c3130352c3131312c3131302c312c33392c302c3235322c3231352c3137372c3136352c31332c32332c312c35312c312c33332c302c3235322c3231352c3137372c3136352c31332c32342c372c39392c3131312c3131302c3131362c3130312c3131302c3131362c312c33392c302c3235322c3231352c3137372c3136352c31332c322c362c3131352c36382c37342c3131362c3132312c37362c312c34302c302c3235322c3231352c3137372c3136352c31332c32362c322c3130352c3130302c312c3131392c362c3131352c36382c37342c3131362c3132312c37362c34302c302c3235322c3231352c3137372c3136352c31332c32362c342c3131302c39372c3130392c3130312c312c3131392c342c36382c3131312c3131302c3130312c34302c302c3235322c3231352c3137372c3136352c31332c32362c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3133312c3134362c3136332c3233362c31322c34302c302c3235322c3231352c3137372c3136352c31332c32362c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c3132352c3133312c3134362c3136332c3233362c31322c34302c302c3235322c3231352c3137372c3136352c31332c32362c31302c3130352c3131352c39352c3131322c3131342c3130352c3130392c39372c3131342c3132312c312c3132312c34302c302c3235322c3231352c3137372c3136352c31332c32362c322c3131362c3132312c312c3132352c352c33392c302c3235322c3231352c3137372c3136352c31332c32362c31312c3131362c3132312c3131322c3130312c39352c3131312c3131322c3131362c3130352c3131312c3131302c312c33392c302c3235322c3231352c3137372c3136352c31332c33332c312c35332c312c33392c302c3235322c3231352c3137372c3136352c31332c332c33362c35362c3130312c34382c35342c35302c3130322c35342c34392c34352c3130302c35352c39372c3130312c34352c35322c3130322c35322c39382c34352c35362c35342c35372c39392c34352c3130322c35322c35322c39392c35322c35312c34392c35322c35372c35312c35372c35372c312c34302c302c3235322c3231352c3137372c3136352c31332c33352c322c3130352c3130302c312c3131392c33362c35362c3130312c34382c35342c35302c3130322c35342c34392c34352c3130302c35352c39372c3130312c34352c35322c3130322c35322c39382c34352c35362c35342c35372c39392c34352c3130322c35322c35322c39392c35322c35312c34392c35322c35372c35312c35372c35372c34302c302c3235322c3231352c3137372c3136352c31332c33352c31312c3130302c39372c3131362c39372c39382c39372c3131352c3130312c39352c3130352c3130302c312c3131392c33362c35332c39372c35362c34392c39382c35342c35332c39382c34352c3130322c39382c3130312c35362c34352c35322c35332c35322c3130322c34352c39382c35312c3130312c39392c34352c35352c35372c35332c39392c35342c3130322c34392c3130322c35342c35312c3130322c34392c34302c302c3235322c3231352c3137372c3136352c31332c33352c342c3131302c39372c3130392c3130312c312c3131392c382c38352c3131302c3131362c3130352c3131362c3130382c3130312c3130302c34302c302c3235322c3231352c3137372c3136352c31332c33352c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3133312c3134362c3136332c3233362c31322c34302c302c3235322c3231352c3137372c3136352c31332c33352c31312c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c39352c39372c3131362c312c3132352c3133312c3134362c3136332c3233362c31322c33392c302c3235322c3231352c3137372c3136352c31332c33352c31352c3130382c39372c3132312c3131312c3131372c3131362c39352c3131352c3130312c3131362c3131362c3130352c3131302c3130332c3131352c312c34302c302c3235322c3231352c3137372c3136352c31332c33352c362c3130382c39372c3132312c3131312c3131372c3131362c312c3132322c302c302c302c302c302c302c302c302c33392c302c3235322c3231352c3137372c3136352c31332c33352c31342c3130322c3130352c3130312c3130382c3130302c39352c3131352c3130312c3131362c3131362c3130352c3131302c3130332c3131352c312c33392c302c3235322c3231352c3137372c3136352c31332c34332c362c3131332c37302c3131332c3131302c38362c3130312c312c34302c302c3235322c3231352c3137372c3136352c31332c34342c31302c3131382c3130352c3131352c3130352c39382c3130352c3130382c3130352c3131362c3132312c312c3132352c302c34302c302c3235322c3231352c3137372c3136352c31332c34342c352c3131392c3130352c3130302c3131362c3130342c312c3132352c3135302c322c34302c302c3235322c3231352c3137372c3136352c31332c34342c342c3131392c3131342c39372c3131322c312c3132302c33392c302c3235322c3231352c3137372c3136352c31332c34332c362c3131352c36382c37342c3131362c3132312c37362c312c34302c302c3235322c3231352c3137372c3136352c31332c34382c31302c3131382c3130352c3131352c3130352c39382c3130352c3130382c3130352c3131362c3132312c312c3132352c302c34302c302c3235322c3231352c3137372c3136352c31332c34382c342c3131392c3131342c39372c3131322c312c3132302c34302c302c3235322c3231352c3137372c3136352c31332c34382c352c3131392c3130352c3130302c3131362c3130342c312c3132352c3135302c322c33392c302c3235322c3231352c3137372c3136352c31332c34332c362c34392c37312c37362c37302c37312c3131332c312c34302c302c3235322c3231352c3137372c3136352c31332c35322c31302c3131382c3130352c3131352c3130352c39382c3130352c3130382c3130352c3131362c3132312c312c3132352c302c34302c302c3235322c3231352c3137372c3136352c31332c35322c342c3131392c3131342c39372c3131322c312c3132302c34302c302c3235322c3231352c3137372c3136352c31332c35322c352c3131392c3130352c3130302c3131362c3130342c312c3132352c3135302c322c33392c302c3235322c3231352c3137372c3136352c31332c33352c372c3130322c3130352c3130382c3131362c3130312c3131342c3131352c302c33392c302c3235322c3231352c3137372c3136352c31332c33352c362c3130332c3131342c3131312c3131372c3131322c3131352c302c33392c302c3235322c3231352c3137372c3136352c31332c33352c352c3131352c3131312c3131342c3131362c3131352c302c33392c302c3235322c3231352c3137372c3136352c31332c33352c31322c3130322c3130352c3130312c3130382c3130302c39352c3131312c3131342c3130302c3130312c3131342c3131352c302c382c302c3235322c3231352c3137372c3136352c31332c35392c332c3131382c312c322c3130352c3130302c3131392c362c3131332c37302c3131332c3131302c38362c3130312c3131382c312c322c3130352c3130302c3131392c362c34392c37312c37362c37302c37312c3131332c3131382c312c322c3130352c3130302c3131392c362c3131352c36382c37342c3131362c3132312c37362c33392c302c3235322c3231352c3137372c3136352c31332c33352c31302c3131342c3131312c3131392c39352c3131312c3131342c3130302c3130312c3131342c3131352c302c382c302c3235322c3231352c3137372c3136352c31332c36332c332c3131382c322c362c3130342c3130312c3130352c3130332c3130342c3131362c3132352c36302c322c3130352c3130302c3131392c33362c3130312c35312c34392c35312c3130322c34382c34382c35372c34352c34392c39372c35342c39392c34352c35322c3130322c35352c35322c34352c39372c39392c39392c39382c34352c34392c35302c39382c3130312c35302c35342c35322c34392c39372c39372c35342c35312c3131382c322c362c3130342c3130312c3130352c3130332c3130342c3131362c3132352c36302c322c3130352c3130302c3131392c33362c35372c35362c35332c39382c35302c39392c35302c35322c34352c3130312c35362c35302c35342c34352c35322c39372c35352c35332c34352c39372c35362c34392c35302c34352c39382c3130312c34392c35372c35362c34392c35302c35372c34392c39372c39392c35312c3131382c322c322c3130352c3130302c3131392c33362c39382c39372c35322c3130312c3130302c34382c35362c39372c34352c35342c3130302c34382c35342c34352c35322c35302c34382c3130312c34352c35372c34382c39382c34392c34352c3130322c35372c35342c3130312c35342c35312c3130302c35322c35342c35322c3130312c35352c362c3130342c3130312c3130352c3130332c3130342c3131362c3132352c36302c3136312c3235322c3231352c3137372c3136352c31332c32302c312c3136312c3235322c3231352c3137372c3136352c31332c32352c312c3136312c3235322c3231352c3137372c3136352c31332c36372c312c3136312c3235322c3231352c3137372c3136352c31332c36382c312c3136382c3235322c3231352c3137372c3136352c31332c36392c312c3132352c3134382c3134362c3136332c3233362c31322c3136382c3235322c3231352c3137372c3136352c31332c37302c312c3131392c3136322c312c3132332c33342c3131312c3131322c3131362c3130352c3131312c3131302c3131352c33342c35382c39312c3132332c33342c3130352c3130302c33342c35382c33342c35352c35342c39392c3130382c33342c34342c33342c3131302c39372c3130392c3130312c33342c35382c33342c39392c33342c34342c33342c39392c3131312c3130382c3131312c3131342c33342c35382c33342c37362c3130352c3130332c3130342c3131362c38302c3130352c3131302c3130372c33342c3132352c34342c3132332c33342c3130352c3130302c33342c35382c33342c3132312c3130312c3131302c34352c33342c34342c33342c3131302c39372c3130392c3130312c33342c35382c33342c3130382c33342c34342c33342c39392c3131312c3130382c3131312c3131342c33342c35382c33342c38302c3130352c3131302c3130372c33342c3132352c34342c3132332c33342c3130352c3130302c33342c35382c33342c38322c3131322c34382c35332c33342c34342c33342c3131302c39372c3130392c3130312c33342c35382c33342c3130342c33342c34342c33342c39392c3131312c3130382c3131312c3131342c33342c35382c33342c38302c3131372c3131342c3131322c3130382c3130312c33342c3132352c39332c34342c33342c3130302c3130352c3131352c39372c39382c3130382c3130312c39352c39392c3131312c3130382c3131312c3131342c33342c35382c3130322c39372c3130382c3131352c3130312c3132352c312c3235332c3139382c3135372c3136312c31332c302c3136312c3133322c3234332c3138332c3234322c31302c302c312c312c3234372c3234312c3233392c3233332c31322c302c3136312c3231352c3136312c3133362c3233322c31322c302c312c312c3231352c3136312c3133362c3233322c31322c302c3136312c3135352c3232332c3235302c3134382c31342c302c312c312c3136342c3235352c3133352c3135392c31322c302c3136312c3234372c3234312c3233392c3233332c31322c302c312c312c3136302c3136332c3230342c3230302c31312c302c3136312c3233312c3139322c3138392c3234312c31352c302c312c312c3133352c3134322c3234302c3139332c31312c302c3136312c3137382c3135302c3234332c3136312c382c302c312c312c3233302c3233322c3136332c3139302c31312c302c3136312c3138302c3133302c3231352c3232312c352c302c312c312c3231302c3133382c3230362c3133342c31312c302c3136312c3233312c3139322c3138392c3234312c31352c302c312c312c3232302c3137372c3136382c3235332c31302c302c33332c312c342c3130392c3130312c3131362c39372c31322c3130382c39372c3131352c3131362c39352c3131352c3132312c3131302c39392c39352c39372c3131362c32392c312c3231312c3139392c3133312c3235302c31302c302c3136312c3230322c3136332c3230382c3137302c392c302c312c312c3133322c3234332c3138332c3234322c31302c302c3136312c3233302c3233322c3136332c3139302c31312c302c312c312c3138302c3137302c3137352c3135362c31302c302c3136312c3232382c3232392c3232392c3138342c382c302c312c312c3138312c3133362c3136392c3134332c31302c302c3136312c3135312c3232342c3134332c3137312c392c302c312c312c3135312c3232342c3134332c3137312c392c302c3136312c3232392c3133372c3133322c3234372c312c302c312c312c3233352c3136332c3230382c3137302c392c302c3136312c3233322c3233322c3234342c3136322c31352c302c312c312c3230322c3136332c3230382c3137302c392c302c3136312c3234382c3137332c3230362c3230312c322c302c312c312c3232382c3232392c3232392c3138342c382c302c3136312c3137382c3135302c3234332c3136312c382c302c312c312c3233342c3139382c3234332c3138312c382c302c3136312c3234382c3137332c3230362c3230312c322c302c312c312c3137362c3234302c3233302c3137392c382c302c3136312c3135352c3232332c3235302c3134382c31342c302c312c312c3137382c3135302c3234332c3136312c382c302c3136312c3232392c3133372c3133322c3234372c312c302c312c312c3132392c3135332c3139322c3233352c372c302c3136312c3235332c3139382c3135372c3136312c31332c302c312c312c3132382c3132392c3134392c3231352c372c302c3136312c3233322c3233322c3234342c3136322c31352c302c312c312c3234312c3134312c3232332c3137362c372c302c3136312c3230322c3136332c3230382c3137302c392c302c312c312c3134362c3133382c3231332c3234342c362c302c3136312c3133352c3134322c3234302c3139332c31312c302c312c312c3133382c3233312c3232392c3232392c362c302c3136312c3133322c3232352c3134382c3135392c332c302c312c312c3230342c3231322c3235332c3230372c362c302c3136312c3233312c3139322c3138392c3234312c31352c302c312c312c3233302c3136342c3134362c3234392c352c302c3136382c3132392c3135332c3139322c3233352c372c302c312c3131392c33362c35332c39372c35362c34392c39382c35342c35332c39382c34352c3130322c39382c3130312c35362c34352c35322c35332c35322c3130322c34352c39382c35312c3130312c39392c34352c35352c35372c35332c39392c35342c3130322c34392c3130322c35342c35312c3130322c34392c312c3138302c3133302c3231352c3232312c352c302c3136312c3231352c3233392c3138352c3138392c31342c302c312c312c3139352c3232392c3137372c3231322c352c302c3136312c3234382c3137332c3230362c3230312c322c302c312c312c3231352c3233302c3134382c3136332c342c302c3136312c3133312c3231382c3137312c3230372c31332c302c312c322c3138352c3235322c3133372c3235352c332c302c3136312c3232302c3137372c3136382c3235332c31302c32382c332c3136382c3138352c3235322c3133372c3235352c332c322c312c3132322c302c302c302c302c3130322c3139392c332c36342c312c3133322c3232352c3134382c3135392c332c302c3136312c3235322c3231352c3137372c3136352c31332c312c312c312c3232312c3134392c3135382c3232392c322c302c3136312c3137362c3234302c3233302c3137392c382c302c312c312c3133392c3137342c3133352c3230332c322c302c3136312c3138312c3133362c3136392c3134332c31302c302c312c312c3234382c3137332c3230362c3230312c322c302c3136312c3133312c3233382c3136322c3139372c31352c302c312c312c3232392c3133372c3133322c3234372c312c302c3136312c3135382c3231382c3136342c3130302c302c312c312c3135342c3230302c3135312c3130392c302c3136312c3231312c3139392c3133312c3235302c31302c302c312c312c3135382c3231382c3136342c3130302c302c3136312c3233362c3235352c3137392c3135392c31342c302c312c312c3136362c3230342c3135322c35372c302c3136312c3233322c3233322c3234342c3136322c31352c302c312c312c3132392c3233382c3134322c34392c302c33332c312c342c3130392c3130312c3131362c39372c31322c3130382c39372c3131352c3131362c39352c3131352c3132312c3131302c39392c39352c39372c3131362c3137322c312c34382c3132382c3132392c3134392c3231352c372c312c302c312c3132392c3233382c3134322c34392c312c302c3137322c312c3132392c3135332c3139322c3233352c372c312c302c312c3133312c3233382c3136322c3139372c31352c312c302c312c3133322c3232352c3134382c3135392c332c312c302c312c3139352c3232392c3137372c3231322c352c312c302c312c3133312c3231382c3137312c3230372c31332c312c302c312c3133352c3134322c3234302c3139332c31312c312c302c312c3133322c3234332c3138332c3234322c31302c312c302c312c3133382c3233312c3232392c3232392c362c312c302c312c3230322c3136332c3230382c3137302c392c312c302c312c3230342c3231322c3235332c3230372c362c312c302c312c3133392c3137342c3133352c3230332c322c312c302c312c3231302c3133382c3230362c3133342c31312c312c302c312c3231312c3139392c3133312c3235302c31302c312c302c312c3134362c3133382c3231332c3234342c362c312c302c312c3231352c3136312c3133362c3233322c31322c312c302c312c3135312c3232342c3134332c3137312c392c312c302c312c3231352c3233302c3134382c3136332c342c312c302c312c3135342c3230302c3135312c3130392c312c302c312c3135352c3232332c3235302c3134382c31342c312c302c312c3232302c3137372c3136382c3235332c31302c312c302c32392c3232312c3134392c3135382c3232392c322c312c302c312c3135382c3231382c3136342c3130302c312c302c312c3231352c3233392c3138352c3138392c31342c312c302c312c3136302c3136332c3230342c3230302c31312c312c302c312c3136342c3235352c3133352c3135392c31322c312c302c312c3232392c3133372c3133322c3234372c312c312c302c312c3136362c3230342c3135322c35372c312c302c312c3233312c3139322c3138392c3234312c31352c312c302c312c3233322c3233322c3234342c3136322c31352c312c302c312c3232382c3232392c3232392c3138342c382c312c302c312c3233342c3139382c3234332c3138312c382c312c302c312c3233352c3136332c3230382c3137302c392c312c302c312c3233362c3235352c3137392c3135392c31342c312c302c312c3136372c3133392c3234382c3139372c31332c312c302c312c3233302c3233322c3136332c3139302c31312c312c302c312c3137362c3234302c3233302c3137392c382c312c302c312c3234312c3134312c3232332c3137362c372c312c302c312c3137382c3135302c3234332c3136312c382c312c302c312c3138302c3137302c3137352c3135362c31302c312c302c312c3138312c3133362c3136392c3134332c31302c312c302c312c3138302c3133302c3231352c3232312c352c312c302c312c3234372c3234312c3233392c3233332c31322c312c302c312c3234382c3137332c3230362c3230312c322c312c302c312c3138352c3235322c3133372c3235352c332c312c302c332c3235322c3231352c3137372c3136352c31332c342c312c312c32302c312c32352c312c36372c342c3235332c3139382c3135372c3136312c31332c312c302c315d2c2264617461626173655f726f775f636f6c6c616273223a7b2239383562326332342d653832362d346137352d613831322d626531393831323931616333223a5b352c322c3232362c3231392c3136302c3138332c31352c302c3136312c3136392c3137332c3138372c3132382c352c312c312c3136382c3232362c3231392c3136302c3138332c31352c302c312c3132322c302c302c302c302c3130322c3139392c332c36302c312c3234362c3139362c3138322c3230392c31332c302c33332c312c342c3130392c3130312c3131362c39372c31322c3130382c39372c3131352c3131362c39352c3131352c3132312c3131302c39392c39352c39372c3131362c31302c312c3134302c3138302c3232362c3232352c382c302c33332c312c342c3130392c3130312c3131362c39372c31322c3130382c39372c3131352c3131362c39352c3131352c3132312c3131302c39392c39352c39372c3131362c37312c32382c3232312c3134382c3233312c3139322c352c302c33392c312c342c3130302c39372c3131362c39372c342c3130302c39372c3131362c39372c312c33392c312c342c3130302c39372c3131362c39372c342c3130392c3130312c3131362c39372c312c33392c312c342c3130302c39372c3131362c39372c372c39392c3131312c3130392c3130392c3130312c3131302c3131362c302c34302c302c3232312c3134382c3233312c3139322c352c302c322c3130352c3130302c312c3131392c33362c35372c35362c35332c39382c35302c39392c35302c35322c34352c3130312c35362c35302c35342c34352c35322c39372c35352c35332c34352c39372c35362c34392c35302c34352c39382c3130312c34392c35372c35362c34392c35302c35372c34392c39372c39392c35312c34302c302c3232312c3134382c3233312c3139322c352c302c31312c3130302c39372c3131362c39372c39382c39372c3131352c3130312c39352c3130352c3130302c312c3131392c33362c35332c39372c35362c34392c39382c35342c35332c39382c34352c3130322c39382c3130312c35362c34352c35322c35332c35322c3130322c34352c39382c35312c3130312c39392c34352c35352c35372c35332c39392c35342c3130322c34392c3130322c35342c35312c3130322c34392c34302c302c3232312c3134382c3233312c3139322c352c302c362c3130342c3130312c3130352c3130332c3130342c3131362c312c3132352c36302c34302c302c3232312c3134382c3233312c3139322c352c302c31302c3131382c3130352c3131352c3130352c39382c3130352c3130382c3130352c3131362c3132312c312c3132302c34302c302c3232312c3134382c3233312c3139322c352c302c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3133312c3134362c3136332c3233362c31322c33332c302c3232312c3134382c3233312c3139322c352c302c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c33392c302c3232312c3134382c3233312c3139322c352c302c352c39392c3130312c3130382c3130382c3131352c312c3136312c3232312c3134382c3233312c3139322c352c382c312c33392c302c3232312c3134382c3233312c3139322c352c392c362c3131332c37302c3131332c3131302c38362c3130312c312c34302c302c3232312c3134382c3233312c3139322c352c31312c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3134342c3134362c3136332c3233362c31322c34302c302c3232312c3134382c3233312c3139322c352c31312c31302c3130322c3130352c3130312c3130382c3130302c39352c3131362c3132312c3131322c3130312c312c3132352c302c34302c302c3232312c3134382c3233312c3139322c352c31312c342c3130302c39372c3131362c39372c312c3131392c312c3130382c34302c302c3232312c3134382c3233312c3139322c352c31312c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c3132352c3134342c3134362c3136332c3233362c31322c3136312c3232312c3134382c3233312c3139322c352c31302c312c33392c302c3232312c3134382c3233312c3139322c352c392c362c34392c37312c37362c37302c37312c3131332c312c34302c302c3232312c3134382c3233312c3139322c352c31372c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3134372c3134362c3136332c3233362c31322c34302c302c3232312c3134382c3233312c3139322c352c31372c342c3130302c39372c3131362c39372c312c3131392c342c3132312c3130312c3131302c34352c34302c302c3232312c3134382c3233312c3139322c352c31372c31302c3130322c3130352c3130312c3130382c3130302c39352c3131362c3132312c3131322c3130312c312c3132352c332c34302c302c3232312c3134382c3233312c3139322c352c31372c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c3132352c3134372c3134362c3136332c3233362c31322c3136382c3232312c3134382c3233312c3139322c352c31362c312c3132352c3135302c3134362c3136332c3233362c31322c33392c302c3232312c3134382c3233312c3139322c352c392c362c3131352c36382c37342c3131362c3132312c37362c312c34302c302c3232312c3134382c3233312c3139322c352c32332c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3135302c3134362c3136332c3233362c31322c34302c302c3232312c3134382c3233312c3139322c352c32332c31302c3130322c3130352c3130312c3130382c3130302c39352c3131362c3132312c3131322c3130312c312c3132352c352c34302c302c3232312c3134382c3233312c3139322c352c32332c342c3130302c39372c3131362c39372c312c3131392c332c38392c3130312c3131352c34302c302c3232312c3134382c3233312c3139322c352c32332c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c3132352c3135302c3134362c3136332c3233362c31322c312c3136392c3137332c3138372c3132382c352c302c3136312c3234362c3139362c3138322c3230392c31332c392c322c352c3136392c3137332c3138372c3132382c352c312c302c322c3232362c3231392c3136302c3138332c31352c312c302c312c3134302c3138302c3232362c3232352c382c312c302c37312c3232312c3134382c3233312c3139322c352c332c382c312c31302c312c31362c312c3234362c3139362c3138322c3230392c31332c312c302c31305d2c2265333133663030392d316136632d346637342d616363622d313262653236343161613633223a5b352c32382c3134342c3139342c3136382c3230352c31322c302c33392c312c342c3130302c39372c3131362c39372c342c3130302c39372c3131362c39372c312c33392c312c342c3130302c39372c3131362c39372c342c3130392c3130312c3131362c39372c312c33392c312c342c3130302c39372c3131362c39372c372c39392c3131312c3130392c3130392c3130312c3131302c3131362c302c34302c302c3134342c3139342c3136382c3230352c31322c302c322c3130352c3130302c312c3131392c33362c3130312c35312c34392c35312c3130322c34382c34382c35372c34352c34392c39372c35342c39392c34352c35322c3130322c35352c35322c34352c39372c39392c39392c39382c34352c34392c35302c39382c3130312c35302c35342c35322c34392c39372c39372c35342c35312c34302c302c3134342c3139342c3136382c3230352c31322c302c31312c3130302c39372c3131362c39372c39382c39372c3131352c3130312c39352c3130352c3130302c312c3131392c33362c35332c39372c35362c34392c39382c35342c35332c39382c34352c3130322c39382c3130312c35362c34352c35322c35332c35322c3130322c34352c39382c35312c3130312c39392c34352c35352c35372c35332c39392c35342c3130322c34392c3130322c35342c35312c3130322c34392c34302c302c3134342c3139342c3136382c3230352c31322c302c362c3130342c3130312c3130352c3130332c3130342c3131362c312c3132352c36302c34302c302c3134342c3139342c3136382c3230352c31322c302c31302c3131382c3130352c3131352c3130352c39382c3130352c3130382c3130352c3131362c3132312c312c3132302c34302c302c3134342c3139342c3136382c3230352c31322c302c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3133312c3134362c3136332c3233362c31322c33332c302c3134342c3139342c3136382c3230352c31322c302c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c33392c302c3134342c3139342c3136382c3230352c31322c302c352c39392c3130312c3130382c3130382c3131352c312c3136312c3134342c3139342c3136382c3230352c31322c382c312c33392c302c3134342c3139342c3136382c3230352c31322c392c362c3131332c37302c3131332c3131302c38362c3130312c312c34302c302c3134342c3139342c3136382c3230352c31322c31312c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3134342c3134362c3136332c3233362c31322c34302c302c3134342c3139342c3136382c3230352c31322c31312c342c3130302c39372c3131362c39372c312c3131392c312c3130342c34302c302c3134342c3139342c3136382c3230352c31322c31312c31302c3130322c3130352c3130312c3130382c3130302c39352c3131362c3132312c3131322c3130312c312c3132352c302c34302c302c3134342c3139342c3136382c3230352c31322c31312c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c3132352c3134342c3134362c3136332c3233362c31322c3136312c3134342c3139342c3136382c3230352c31322c31302c312c33392c302c3134342c3139342c3136382c3230352c31322c392c362c34392c37312c37362c37302c37312c3131332c312c34302c302c3134342c3139342c3136382c3230352c31322c31372c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3134362c3134362c3136332c3233362c31322c34302c302c3134342c3139342c3136382c3230352c31322c31372c31302c3130322c3130352c3130312c3130382c3130302c39352c3131362c3132312c3131322c3130312c312c3132352c332c34302c302c3134342c3139342c3136382c3230352c31322c31372c342c3130302c39372c3131362c39372c312c3131392c342c38322c3131322c34382c35332c34302c302c3134342c3139342c3136382c3230352c31322c31372c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c3132352c3134362c3134362c3136332c3233362c31322c3136312c3134342c3139342c3136382c3230352c31322c31362c312c33392c302c3134342c3139342c3136382c3230352c31322c392c362c3131352c36382c37342c3131362c3132312c37362c312c34302c302c3134342c3139342c3136382c3230352c31322c32332c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3135302c3134362c3136332c3233362c31322c33332c302c3134342c3139342c3136382c3230352c31322c32332c31302c3130322c3130352c3130312c3130382c3130302c39352c3131362c3132312c3131322c3130312c312c33332c302c3134342c3139342c3136382c3230352c31322c32332c342c3130302c39372c3131362c39372c312c33332c302c3134342c3139342c3136382c3230352c31322c32332c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c312c3133382c3137392c3133362c3138382c372c302c33332c312c342c3130392c3130312c3131362c39372c31322c3130382c39372c3131352c3131362c39352c3131352c3132312c3131302c39392c39352c39372c3131362c31322c372c3230332c3135362c3230322c3231372c352c302c3136312c3134342c3139342c3136382c3230352c31322c32322c312c3136382c3134342c3139342c3136382c3230352c31322c32352c312c3132322c302c302c302c302c302c302c302c352c3136312c3134342c3139342c3136382c3230352c31322c32362c312c3136312c3134342c3139342c3136382c3230352c31322c32372c312c3136382c3230332c3135362c3230322c3231372c352c302c312c3132322c302c302c302c302c3130322c3139382c3232372c3230312c3136382c3230332c3135362c3230322c3231372c352c322c312c3131392c332c38392c3130312c3131352c3136382c3230332c3135362c3230322c3231372c352c332c312c3132322c302c302c302c302c3130322c3139382c3232372c3230312c312c3233352c3135362c3137352c3234362c322c302c33332c312c342c3130392c3130312c3131362c39372c31322c3130382c39372c3131352c3131362c39352c3131352c3132312c3131302c39392c39352c39372c3131362c36392c322c3137312c3138312c3133302c3135352c322c302c3136312c3133382c3137392c3133362c3138382c372c31312c322c3136382c3137312c3138312c3133302c3135352c322c312c312c3132322c302c302c302c302c3130322c3139392c332c36302c352c3134342c3139342c3136382c3230352c31322c352c382c312c31302c312c31362c312c32322c312c32352c332c3133382c3137392c3133362c3138382c372c312c302c31322c3233352c3135362c3137352c3234362c322c312c302c36392c3230332c3135362c3230322c3231372c352c322c302c312c322c322c3137312c3138312c3133302c3135352c322c312c302c325d2c2262613465643038612d366430362d343230652d393062312d663936653633643436346537223a5b352c322c3234352c3133302c3138312c3232302c31352c302c3136312c3135362c3233302c3232362c3135312c372c312c312c3136382c3234352c3133302c3138312c3232302c31352c302c312c3132322c302c302c302c302c3130322c3139392c332c36302c312c3135362c3233302c3232362c3135312c372c302c3136312c3234302c3135312c3234342c3137332c352c392c322c312c3234302c3135312c3234342c3137332c352c302c33332c312c342c3130392c3130312c3131362c39372c31322c3130382c39372c3131352c3131362c39352c3131352c3132312c3131302c39392c39352c39372c3131362c31302c312c3231352c3132382c3138392c3133302c322c302c33332c312c342c3130392c3130312c3131362c39372c31322c3130382c39372c3131352c3131362c39352c3131352c3132312c3131302c39392c39352c39372c3131362c36372c32382c3135322c3133382c3233302c36302c302c33392c312c342c3130302c39372c3131362c39372c342c3130302c39372c3131362c39372c312c33392c312c342c3130302c39372c3131362c39372c342c3130392c3130312c3131362c39372c312c33392c312c342c3130302c39372c3131362c39372c372c39392c3131312c3130392c3130392c3130312c3131302c3131362c302c34302c302c3135322c3133382c3233302c36302c302c322c3130352c3130302c312c3131392c33362c39382c39372c35322c3130312c3130302c34382c35362c39372c34352c35342c3130302c34382c35342c34352c35322c35302c34382c3130312c34352c35372c34382c39382c34392c34352c3130322c35372c35342c3130312c35342c35312c3130302c35322c35342c35322c3130312c35352c34302c302c3135322c3133382c3233302c36302c302c31312c3130302c39372c3131362c39372c39382c39372c3131352c3130312c39352c3130352c3130302c312c3131392c33362c35332c39372c35362c34392c39382c35342c35332c39382c34352c3130322c39382c3130312c35362c34352c35322c35332c35322c3130322c34352c39382c35312c3130312c39392c34352c35352c35372c35332c39392c35342c3130322c34392c3130322c35342c35312c3130322c34392c34302c302c3135322c3133382c3233302c36302c302c362c3130342c3130312c3130352c3130332c3130342c3131362c312c3132352c36302c34302c302c3135322c3133382c3233302c36302c302c31302c3131382c3130352c3131352c3130352c39382c3130352c3130382c3130352c3131362c3132312c312c3132302c34302c302c3135322c3133382c3233302c36302c302c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3133312c3134362c3136332c3233362c31322c33332c302c3135322c3133382c3233302c36302c302c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c33392c302c3135322c3133382c3233302c36302c302c352c39392c3130312c3130382c3130382c3131352c312c3136312c3135322c3133382c3233302c36302c382c312c33392c302c3135322c3133382c3233302c36302c392c362c3131332c37302c3131332c3131302c38362c3130312c312c34302c302c3135322c3133382c3233302c36302c31312c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3134352c3134362c3136332c3233362c31322c34302c302c3135322c3133382c3233302c36302c31312c342c3130302c39372c3131362c39372c312c3131392c312c39392c34302c302c3135322c3133382c3233302c36302c31312c31302c3130322c3130352c3130312c3130382c3130302c39352c3131362c3132312c3131322c3130312c312c3132352c302c34302c302c3135322c3133382c3233302c36302c31312c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c3132352c3134352c3134362c3136332c3233362c31322c3136312c3135322c3133382c3233302c36302c31302c312c33392c302c3135322c3133382c3233302c36302c392c362c34392c37312c37362c37302c37312c3131332c312c34302c302c3135322c3133382c3233302c36302c31372c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3134382c3134362c3136332c3233362c31322c34302c302c3135322c3133382c3233302c36302c31372c342c3130302c39372c3131362c39372c312c3131392c342c35352c35342c39392c3130382c34302c302c3135322c3133382c3233302c36302c31372c31302c3130322c3130352c3130312c3130382c3130302c39352c3131362c3132312c3131322c3130312c312c3132352c332c34302c302c3135322c3133382c3233302c36302c31372c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c3132352c3134382c3134362c3136332c3233362c31322c3136382c3135322c3133382c3233302c36302c31362c312c3132352c3134392c3134362c3136332c3233362c31322c33392c302c3135322c3133382c3233302c36302c392c362c3131352c36382c37342c3131362c3132312c37362c312c34302c302c3135322c3133382c3233302c36302c32332c31302c39392c3131342c3130312c39372c3131362c3130312c3130302c39352c39372c3131362c312c3132352c3134392c3134362c3136332c3233362c31322c34302c302c3135322c3133382c3233302c36302c32332c342c3130302c39372c3131362c39372c312c3131392c332c38392c3130312c3131352c34302c302c3135322c3133382c3233302c36302c32332c31302c3130322c3130352c3130312c3130382c3130302c39352c3131362c3132312c3131322c3130312c312c3132352c352c34302c302c3135322c3133382c3233302c36302c32332c31332c3130382c39372c3131352c3131362c39352c3130392c3131312c3130302c3130352c3130322c3130352c3130312c3130302c312c3132352c3134392c3134362c3136332c3233362c31322c352c3135322c3133382c3233302c36302c332c382c312c31302c312c31362c312c3234302c3135312c3234342c3137332c352c312c302c31302c3135362c3233302c3232362c3135312c372c312c302c322c3234352c3133302c3138312c3232302c31352c312c302c312c3231352c3132382c3138392c3133302c322c312c302c36375d7d2c2264617461626173655f726f775f646f63756d656e745f636f6c6c616273223a7b7d2c2276697369626c655f64617461626173655f766965775f696473223a5b2264383538396539382d383866632d343265342d383838632d623033333338626632326262225d2c2264617461626173655f72656c6174696f6e73223a7b2233663964653338372d636131382d343232622d393262662d306263353231613461653339223a2233396332633365662d386431332d343339632d616166392d336534623434663363313434222c2235613831623635622d666265382d343534662d623365632d373935633666316636336631223a2238653036326636312d643761652d346634622d383639632d663434633433313439333939227d7d"; diff --git a/tests/workspace/workspace_folder.rs b/tests/workspace/workspace_folder.rs new file mode 100644 index 000000000..c0931bbd7 --- /dev/null +++ b/tests/workspace/workspace_folder.rs @@ -0,0 +1,14 @@ +use client_api_test::generate_unique_registered_user_client; + +#[tokio::test] +async fn get_workpace_folder() { + let (c, _user) = generate_unique_registered_user_client().await; + let workspaces = c.get_workspaces().await.unwrap(); + assert_eq!(workspaces.len(), 1); + let workspace_id = workspaces[0].workspace_id.to_string(); + + let folder_view = c.get_workspace_folder(&workspace_id, None).await.unwrap(); + assert_eq!(folder_view.name, "Workspace"); + assert_eq!(folder_view.children[0].name, "General"); + assert_eq!(folder_view.children[0].children.len(), 0); +} diff --git a/tests/yrs_version/folder_test.rs b/tests/yrs_version/folder_test.rs index 0db1a6d51..4b3f1d32a 100644 --- a/tests/yrs_version/folder_test.rs +++ b/tests/yrs_version/folder_test.rs @@ -1,4 +1,3 @@ -use collab::core::collab::DataSource; use collab::core::origin::CollabOrigin; use collab::entity::EncodedCollab; use collab_folder::Folder; @@ -25,7 +24,7 @@ fn load_yrs_0172_version_folder_using_current_yrs_version() { let folder = Folder::from_collab_doc_state( 322319512080748544, CollabOrigin::Empty, - DataSource::DocStateV1(encode_collab.doc_state.to_vec()), + encode_collab.into(), "fake_id", // just use fake id vec![], )