return paths

This commit is contained in:
Andras Schmelczer 2026-04-25 08:40:40 +01:00
parent c9cf3239db
commit aecbcd1d2c
12 changed files with 20 additions and 136 deletions

View file

@ -1,9 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Like [`DocumentVersion`] but without the `relative_path`.
* Used only in create/update responses when the server had to merge the
* client's content with a newer remote version and therefore must echo
* the merged content back.
*/
export type DocumentUpdateMergedContent = { vaultUpdateId: number, documentId: string, updatedDate: string, contentBase64: string, isDeleted: boolean, userId: string, deviceId: string, };

View file

@ -1,9 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Like [`DocumentVersionWithoutContent`] but without the `relative_path`.
* Used only in create/update responses where the client already tracks
* the path locally (the server is the source of truth for the
* document identity, not its path).
*/
export type DocumentUpdateMetadata = { vaultUpdateId: number, documentId: string, updatedDate: string, isDeleted: boolean, userId: string, deviceId: string, contentSize: number, };

View file

@ -1,8 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DocumentUpdateMergedContent } from "./DocumentUpdateMergedContent";
import type { DocumentUpdateMetadata } from "./DocumentUpdateMetadata";
import type { DocumentVersion } from "./DocumentVersion";
import type { DocumentVersionWithoutContent } from "./DocumentVersionWithoutContent";
/**
* Response to a create/update document request.
*/
export type DocumentUpdateResponse = { "type": "FastForwardUpdate" } & DocumentUpdateMetadata | { "type": "MergingUpdate" } & DocumentUpdateMergedContent;
export type DocumentUpdateResponse = { "type": "FastForwardUpdate" } & DocumentVersionWithoutContent | { "type": "MergingUpdate" } & DocumentVersion;

View file

@ -8,7 +8,7 @@ import { assertSetContainsExactly } from "../utils/assert-set-contains-exactly";
import type { FileSystemOperations } from "./filesystem-operations";
import type { TextWithCursors } from "reconcile-text";
import type { ServerConfig, ServerConfigData } from "../services/server-config";
import { isConflictPath } from "../sync-operations/conflict-path";
import { CONFLICT_PATH_REGEX } from "../sync-operations/conflict-path";
class MockServerConfig implements Pick<ServerConfig, "getConfig"> {
public async getConfig(): Promise<ServerConfigData> {
@ -108,7 +108,7 @@ function singleConflictPath(
`expected exactly one conflict-path entry, got ${JSON.stringify(conflicts)}`
);
assert.ok(
isConflictPath(conflicts[0]),
CONFLICT_PATH_REGEX.test(conflicts[0]),
`expected ${conflicts[0]} to match the conflict-path pattern`
);
return conflicts[0];
@ -195,7 +195,7 @@ describe("File operations", () => {
(name) => name !== ".gitignore" && name !== ".config.json"
);
assert.equal(conflicts.length, 2);
assert.ok(conflicts.every(isConflictPath));
assert.ok(conflicts.every((c) => CONFLICT_PATH_REGEX.test(c)));
assert.ok(conflicts.some((c) => c.endsWith("-.gitignore")));
assert.ok(conflicts.some((c) => c.endsWith("-.config.json")));
});
@ -209,7 +209,7 @@ describe("File operations", () => {
const conflicts = Array.from(fs.names).filter((n) => n !== "x");
assert.equal(conflicts.length, 2);
assert.ok(conflicts.every(isConflictPath));
assert.ok(conflicts.every((c) => CONFLICT_PATH_REGEX.test(c)));
assert.notEqual(
conflicts[0],
conflicts[1],

View file

@ -64,7 +64,7 @@ export class FileOperations {
*
* If a file is already there, it is moved aside to a `conflict-<uuid>-<name>`
* path in the same directory. The sync layer treats conflict-named files
* as invisible (see `isConflictPath`), so no events are enqueued and no
* as invisible (see `CONFLICT_PATH_REGEX`), so no events are enqueued and no
* document records are touched any pre-existing record or pending
* events for the displaced path are left behind for the caller to
* overwrite as part of whatever operation prompted the displacement.

View file

@ -1,9 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Like [`DocumentVersion`] but without the `relative_path`.
* Used only in create/update responses when the server had to merge the
* client's content with a newer remote version and therefore must echo
* the merged content back.
*/
export interface DocumentUpdateMergedContent { vaultUpdateId: number, documentId: string, updatedDate: string, contentBase64: string, isDeleted: boolean, userId: string, deviceId: string, }

View file

@ -1,9 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Like [`DocumentVersionWithoutContent`] but without the `relative_path`.
* Used only in create/update responses where the client already tracks
* the path locally (the server is the source of truth for the
* document identity, not its path).
*/
export interface DocumentUpdateMetadata { vaultUpdateId: number, documentId: string, updatedDate: string, isDeleted: boolean, userId: string, deviceId: string, contentSize: number, }

View file

@ -1,8 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DocumentUpdateMergedContent } from "./DocumentUpdateMergedContent";
import type { DocumentUpdateMetadata } from "./DocumentUpdateMetadata";
import type { DocumentVersion } from "./DocumentVersion";
import type { DocumentVersionWithoutContent } from "./DocumentVersionWithoutContent";
/**
* Response to a create/update document request.
*/
export type DocumentUpdateResponse = { "type": "FastForwardUpdate" } & DocumentUpdateMetadata | { "type": "MergingUpdate" } & DocumentUpdateMergedContent;
export type DocumentUpdateResponse = { "type": "FastForwardUpdate" } & DocumentVersionWithoutContent | { "type": "MergingUpdate" } & DocumentVersion;

View file

@ -1,6 +1,6 @@
import { describe, it } from "node:test";
import assert from "node:assert";
import { buildConflictFileName, isConflictPath } from "./conflict-path";
import { buildConflictFileName, CONFLICT_PATH_REGEX } from "./conflict-path";
describe("buildConflictFileName", () => {
it("truncates to the filesystem byte limit while preserving the extension", () => {
@ -59,20 +59,20 @@ describe("buildConflictFileName", () => {
});
});
describe("isConflictPath", () => {
describe("CONFLICT_PATH_REGEX", () => {
it("does not misclassify user-authored names that start with `conflict-`", () => {
assert.strictEqual(isConflictPath("conflict-resolution.md"), false);
assert.strictEqual(CONFLICT_PATH_REGEX.test("conflict-resolution.md"), false);
});
it("only inspects the final path segment", () => {
assert.strictEqual(
isConflictPath(
CONFLICT_PATH_REGEX.test(
"conflict-12345678-1234-1234-1234-123456789abc-x/note.md"
),
false
);
assert.strictEqual(
isConflictPath(
CONFLICT_PATH_REGEX.test(
"a/b/conflict-12345678-1234-1234-1234-123456789abc-note.md"
),
true
@ -80,6 +80,6 @@ describe("isConflictPath", () => {
});
it("round-trips with buildConflictFileName", () => {
assert.strictEqual(isConflictPath(buildConflictFileName("note.md")), true);
assert.strictEqual(CONFLICT_PATH_REGEX.test(buildConflictFileName("note.md")), true);
});
});

View file

@ -1,5 +1,3 @@
import type { RelativePath } from "./types";
// Local-only files displaced by `FileOperations.ensureClearPath` are named
// `conflict-<uuid>-<originalName>`. The UUID is a full RFC-4122 v4 value so
// a user-authored filename that happens to start with `conflict-` doesn't
@ -55,14 +53,3 @@ function truncateFileNameToByteLimit(
}
return truncatedStem + extension;
}
/**
* Is `path`'s final segment a conflict-displaced filename?
*
* Any sync code that would otherwise create/update/delete/sync the path
* should short-circuit when this returns true: conflict-displaced files are
* strictly local and must stay invisible to the server.
*/
export function isConflictPath(path: RelativePath): boolean {
return CONFLICT_PATH_REGEX.test(path);
}

View file

@ -78,72 +78,6 @@ pub struct DocumentVersion {
pub device_id: DeviceId,
}
/// Like [`DocumentVersionWithoutContent`] but without the `relative_path`.
/// Used only in create/update responses where the client already tracks
/// the path locally (the server is the source of truth for the
/// document identity, not its path).
#[derive(TS, Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DocumentUpdateMetadata {
#[ts(type = "number")]
pub vault_update_id: VaultUpdateId,
pub document_id: DocumentId,
pub updated_date: DateTime<Utc>,
pub is_deleted: bool,
pub user_id: UserId,
pub device_id: DeviceId,
#[ts(type = "number")]
pub content_size: u64,
}
impl From<StoredDocumentVersion> for DocumentUpdateMetadata {
fn from(value: StoredDocumentVersion) -> Self {
Self {
vault_update_id: value.vault_update_id,
document_id: value.document_id,
updated_date: value.updated_date,
is_deleted: value.is_deleted,
user_id: value.user_id,
device_id: value.device_id,
content_size: value.content.len() as u64,
}
}
}
/// Like [`DocumentVersion`] but without the `relative_path`.
/// Used only in create/update responses when the server had to merge the
/// client's content with a newer remote version and therefore must echo
/// the merged content back.
#[derive(TS, Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DocumentUpdateMergedContent {
#[ts(type = "number")]
pub vault_update_id: VaultUpdateId,
pub document_id: DocumentId,
pub updated_date: DateTime<Utc>,
pub content_base64: String,
pub is_deleted: bool,
pub user_id: UserId,
pub device_id: DeviceId,
}
impl From<StoredDocumentVersion> for DocumentUpdateMergedContent {
fn from(value: StoredDocumentVersion) -> Self {
Self {
vault_update_id: value.vault_update_id,
document_id: value.document_id,
updated_date: value.updated_date,
content_base64: STANDARD.encode(&value.content),
is_deleted: value.is_deleted,
user_id: value.user_id,
device_id: value.device_id,
}
}
}
/// Row struct for vault history queries (used by `sqlx::query_as!`)
#[derive(Debug)]
pub struct VaultHistoryRow {

View file

@ -3,8 +3,7 @@ use serde::{self, Serialize};
use ts_rs::TS;
use crate::app_state::database::models::{
DocumentUpdateMergedContent, DocumentUpdateMetadata, DocumentVersionWithoutContent,
VaultUpdateId,
DocumentVersion, DocumentVersionWithoutContent, VaultUpdateId,
};
/// Response to a ping request.
@ -75,9 +74,9 @@ pub enum DocumentUpdateResponse {
/// Returned when the created/updated document's content is the same as was
/// sent in the create/update request and thus the response doesn't contain
/// the content because the client must already have it.
FastForwardUpdate(DocumentUpdateMetadata),
FastForwardUpdate(DocumentVersionWithoutContent),
/// Returned when the created/updated document's content is different from
/// what was sent in the create/update request.
MergingUpdate(DocumentUpdateMergedContent),
MergingUpdate(DocumentVersion),
}