feat(server): synthetic root doc (#14794)
This commit is contained in:
@@ -832,6 +832,7 @@ export interface CopilotWorkspaceIgnoredDocTypeEdge {
|
||||
}
|
||||
|
||||
export interface CreateChatMessageInput {
|
||||
/** @deprecated use blobs */
|
||||
attachments?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
blob?: InputMaybe<Scalars['Upload']['input']>;
|
||||
blobs?: InputMaybe<Array<Scalars['Upload']['input']>>;
|
||||
@@ -853,6 +854,7 @@ export interface CreateChatSessionInput {
|
||||
export interface CreateCheckoutSessionInput {
|
||||
args?: InputMaybe<Scalars['JSONObject']['input']>;
|
||||
coupon?: InputMaybe<Scalars['String']['input']>;
|
||||
/** @deprecated not required anymore */
|
||||
idempotencyKey?: InputMaybe<Scalars['String']['input']>;
|
||||
plan?: InputMaybe<SubscriptionPlan>;
|
||||
recurring?: InputMaybe<SubscriptionRecurring>;
|
||||
|
||||
@@ -17,5 +17,6 @@ pub use read::{
|
||||
parse_doc_from_binary, parse_doc_to_markdown, parse_page_doc, parse_workspace_doc,
|
||||
};
|
||||
pub use write::{
|
||||
add_doc_to_root_doc, build_full_doc, update_doc, update_doc_properties, update_doc_title, update_root_doc_meta_title,
|
||||
add_doc_to_root_doc, build_full_doc, build_public_root_doc, update_doc, update_doc_properties, update_doc_title,
|
||||
update_root_doc_meta_title,
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ mod update;
|
||||
pub use create::build_full_doc;
|
||||
pub use doc_meta::{update_doc_title, update_root_doc_meta_title};
|
||||
pub use doc_properties::update_doc_properties;
|
||||
pub use root_doc::add_doc_to_root_doc;
|
||||
pub use root_doc::{add_doc_to_root_doc, build_public_root_doc};
|
||||
pub use update::update_doc;
|
||||
use y_octo::{Any, Doc, Map, Value};
|
||||
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use y_octo::Array;
|
||||
|
||||
use super::*;
|
||||
|
||||
const DEFAULT_DOC_TITLE: &str = "Untitled";
|
||||
const SIMPLE_PAGE_META_KEYS: &[&str] = &[
|
||||
"id",
|
||||
"title",
|
||||
"createDate",
|
||||
"updatedDate",
|
||||
"trash",
|
||||
"trashDate",
|
||||
"headerImage",
|
||||
];
|
||||
|
||||
fn any_to_value(doc: &Doc, any: Any) -> Result<Value, ParseError> {
|
||||
match any {
|
||||
@@ -104,3 +115,177 @@ pub fn add_doc_to_root_doc(root_doc_bin: Vec<u8>, doc_id: &str, title: Option<&s
|
||||
// Encode only the changes (delta) since state_before
|
||||
Ok(doc.encode_state_as_update_v1(&state_before)?)
|
||||
}
|
||||
|
||||
fn insert_page_stub(doc: &Doc, pages: &mut Array, doc_id: &str, title: Option<&str>) -> Result<(), ParseError> {
|
||||
let page_map = doc.create_map()?;
|
||||
let idx = pages.len();
|
||||
pages.insert(idx, Value::Map(page_map))?;
|
||||
|
||||
if let Some(mut inserted_page) = pages.get(idx).and_then(|v| v.to_map()) {
|
||||
inserted_page.insert("id".to_string(), Any::String(doc_id.to_string()))?;
|
||||
inserted_page.insert(
|
||||
"title".to_string(),
|
||||
Any::String(title.unwrap_or(DEFAULT_DOC_TITLE).to_string()),
|
||||
)?;
|
||||
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as i64)
|
||||
.unwrap_or(0);
|
||||
inserted_page.insert("createDate".to_string(), Any::Float64((timestamp as f64).into()))?;
|
||||
|
||||
let tags = doc.create_array()?;
|
||||
inserted_page.insert("tags".to_string(), Value::Array(tags))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert_simple_array(doc: &Doc, values: &[Any]) -> Result<Value, ParseError> {
|
||||
let mut array = doc.create_array()?;
|
||||
for value in values {
|
||||
match value {
|
||||
Any::Array(_) | Any::Object(_) => continue,
|
||||
_ => array.push(Value::Any(value.clone()))?,
|
||||
}
|
||||
}
|
||||
Ok(Value::Array(array))
|
||||
}
|
||||
|
||||
fn insert_page_from_any(doc: &Doc, pages: &mut Array, page: Any) -> Result<Option<String>, ParseError> {
|
||||
let Any::Object(page) = page else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let Some(page_id) = page.get("id").and_then(|value| match value {
|
||||
Any::String(value) => Some(value.clone()),
|
||||
_ => None,
|
||||
}) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let page_map = doc.create_map()?;
|
||||
let idx = pages.len();
|
||||
pages.insert(idx, Value::Map(page_map))?;
|
||||
|
||||
let Some(mut inserted_page) = pages.get(idx).and_then(|v| v.to_map()) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
for key in SIMPLE_PAGE_META_KEYS {
|
||||
let Some(value) = page.get(*key) else {
|
||||
continue;
|
||||
};
|
||||
match value {
|
||||
Any::Array(values) => {
|
||||
inserted_page.insert((*key).to_string(), insert_simple_array(doc, values)?)?;
|
||||
}
|
||||
Any::Object(_) => continue,
|
||||
_ => {
|
||||
inserted_page.insert((*key).to_string(), Value::Any(value.clone()))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Any::Array(tags)) = page.get("tags") {
|
||||
inserted_page.insert("tags".to_string(), insert_simple_array(doc, tags)?)?;
|
||||
}
|
||||
|
||||
Ok(Some(page_id))
|
||||
}
|
||||
|
||||
pub fn build_public_root_doc(root_doc_bin: &[u8], doc_metas: &[(&str, Option<&str>)]) -> Result<Vec<u8>, ParseError> {
|
||||
let source = load_doc_or_new(root_doc_bin)?;
|
||||
let public_doc_ids = doc_metas
|
||||
.iter()
|
||||
.map(|(doc_id, _title)| (*doc_id).to_string())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let doc = Doc::default();
|
||||
let mut meta = doc.get_or_create_map("meta")?;
|
||||
let mut pages = ensure_pages_array(&doc, &mut meta)?;
|
||||
let mut copied = HashSet::new();
|
||||
|
||||
if let Ok(source_meta) = source.get_map("meta") {
|
||||
let source_pages_value = source_meta.get("pages");
|
||||
|
||||
if let Some(source_pages) = source_pages_value.as_ref().and_then(|value| value.to_array()) {
|
||||
for page_val in source_pages.iter() {
|
||||
let Some(page) = page_val.to_map() else {
|
||||
continue;
|
||||
};
|
||||
let Some(page_id) = get_string(&page, "id") else {
|
||||
continue;
|
||||
};
|
||||
if !public_doc_ids.contains(&page_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let page_object = Any::Object(
|
||||
page
|
||||
.iter()
|
||||
.filter_map(|(key, value)| value.to_any().map(|any| (key.to_string(), any)))
|
||||
.collect(),
|
||||
);
|
||||
if let Some(inserted_page_id) = insert_page_from_any(&doc, &mut pages, page_object)? {
|
||||
copied.insert(inserted_page_id);
|
||||
}
|
||||
}
|
||||
} else if let Some(Any::Array(entries)) = source_pages_value.and_then(|value| value.to_any()) {
|
||||
for entry in entries {
|
||||
let Any::Object(page) = entry.clone() else {
|
||||
continue;
|
||||
};
|
||||
let Some(Any::String(page_id)) = page.get("id") else {
|
||||
continue;
|
||||
};
|
||||
if !public_doc_ids.contains(page_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(inserted_page_id) = insert_page_from_any(&doc, &mut pages, entry)? {
|
||||
copied.insert(inserted_page_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (doc_id, title) in doc_metas {
|
||||
if copied.contains(*doc_id) {
|
||||
continue;
|
||||
}
|
||||
insert_page_stub(&doc, &mut pages, doc_id, *title)?;
|
||||
}
|
||||
|
||||
Ok(doc.encode_update_v1()?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::doc_parser::get_doc_ids_from_binary;
|
||||
|
||||
#[test]
|
||||
fn test_build_public_root_doc_filters_private_pages() {
|
||||
let root = add_doc_to_root_doc(Vec::new(), "public-doc", Some("Public")).expect("create public entry");
|
||||
let update = add_doc_to_root_doc(root.clone(), "private-doc", Some("Private")).expect("create private entry");
|
||||
|
||||
let mut merged = load_doc_or_new(&root).expect("load root");
|
||||
merged
|
||||
.apply_update_from_binary_v1(&update)
|
||||
.expect("apply second update");
|
||||
let merged_bin = merged.encode_update_v1().expect("encode merged");
|
||||
|
||||
let public_root = build_public_root_doc(
|
||||
&merged_bin,
|
||||
&[("public-doc", Some("Public")), ("missing-public-doc", Some("Fallback"))],
|
||||
)
|
||||
.expect("build public root");
|
||||
|
||||
let doc_ids = get_doc_ids_from_binary(public_root, false).expect("read public root doc ids");
|
||||
assert_eq!(
|
||||
doc_ids,
|
||||
vec!["public-doc".to_string(), "missing-public-doc".to_string()]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,12 @@ import { HttpConnection } from './http';
|
||||
|
||||
interface CloudDocStorageOptions extends DocStorageOptions {
|
||||
serverBaseUrl: string;
|
||||
publicRootDocId?: string;
|
||||
}
|
||||
|
||||
const isShareModePrivateSystemDoc = (docId: string) =>
|
||||
docId.startsWith('db$') || docId.startsWith('userdata$');
|
||||
|
||||
export class StaticCloudDocStorage extends DocStorageBase<CloudDocStorageOptions> {
|
||||
static readonly identifier = 'StaticCloudDocStorage';
|
||||
|
||||
@@ -46,15 +50,26 @@ export class StaticCloudDocStorage extends DocStorageBase<CloudDocStorageOptions
|
||||
docId: string
|
||||
): Promise<DocRecord | null> {
|
||||
try {
|
||||
const arrayBuffer = await this.connection.fetchArrayBuffer(
|
||||
`/api/workspaces/${this.spaceId}/docs/${docId}`,
|
||||
{
|
||||
priority: 'high',
|
||||
headers: {
|
||||
Accept: 'application/octet-stream', // this is necessary for ios native fetch to return arraybuffer
|
||||
},
|
||||
}
|
||||
);
|
||||
if (
|
||||
this.options.publicRootDocId &&
|
||||
docId !== this.spaceId &&
|
||||
isShareModePrivateSystemDoc(docId)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const path =
|
||||
docId === this.spaceId && this.options.publicRootDocId
|
||||
? `/api/workspaces/${this.spaceId}/public-docs/${this.options.publicRootDocId}/root-doc`
|
||||
: this.options.publicRootDocId
|
||||
? `/api/workspaces/${this.spaceId}/public-docs/${docId}`
|
||||
: `/api/workspaces/${this.spaceId}/docs/${docId}`;
|
||||
const arrayBuffer = await this.connection.fetchArrayBuffer(path, {
|
||||
priority: 'high',
|
||||
headers: {
|
||||
Accept: 'application/octet-stream', // this is necessary for ios native fetch to return arraybuffer
|
||||
},
|
||||
});
|
||||
if (!arrayBuffer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user