Access and entity words are core constants: access::*, entity::*

"owner"/"edit"/"view" and the entity names "note"/"label"/"attachment"/"preview"
were literals at about thirty sites across store, pull and push, including match
arms whose spelling had to agree with the rows a different module wrote.
models::access and models::entity now name them, and push names its two ops.
SQL text keeps its literals; Rust-side comparisons and writes read the constants.

DRY pass #2, batch 2, F7 (#5372).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 14:22:49 -04:00
co-authored by Claude Opus 5.5
parent 8aa854d60b
commit 63dd206b91
4 changed files with 56 additions and 33 deletions
+5 -4
View File
@@ -13,6 +13,7 @@ use super::blobs::BlobStore;
use super::client;
use super::state;
use super::wire;
use crate::local::models::{access, entity};
use crate::local::{now, Db};
/// Backstop against a server that never stops saying `has_more`. At the server's
@@ -248,9 +249,9 @@ fn upsert_note(conn: &Connection, note: &wire::Note) -> rusqlite::Result<()> {
// never changes, and the server's copy is the same value anyway.
// A server without `shares` sends no permission, and every note it sends is ours.
let permission = match note.permission.as_deref() {
Some("edit") => "edit",
Some("view") => "view",
_ => "owner",
Some(access::EDIT) => access::EDIT,
Some(access::VIEW) => access::VIEW,
_ => access::OWNER,
};
let (shared_by_id, shared_by_name) = match &note.shared_by {
Some(by) => (Some(by.id.as_str()), Some(by.display_name.as_str())),
@@ -330,7 +331,7 @@ fn replace_attachments(conn: &Connection, note: &wire::Note) -> rusqlite::Result
params![note.id],
)?;
for (index, att) in note.attachments.iter().enumerate() {
if removed_here(conn, "attachment", &att.id)? {
if removed_here(conn, entity::ATTACHMENT, &att.id)? {
continue;
}
// The server listing an id this device is still waiting to upload means the
+23 -18
View File
@@ -15,6 +15,7 @@ use serde::{Deserialize, Serialize};
use super::blobs::BlobStore;
use super::client::{self, UploadError};
use super::state;
use crate::local::models::{access, entity};
use crate::local::Db;
/// The server rejects a batch larger than this (`MAX_PUSH` in `sync.py`).
@@ -61,6 +62,10 @@ impl PushSummary {
// --- outgoing shapes ---------------------------------------------------------
/// A change's operation on the wire.
const UPSERT: &str = "upsert";
const DELETE: &str = "delete";
/// One entry in the `changes` array. Notes and labels share the envelope; serde skips
/// the fields that don't apply, so the server sees exactly the shape docs/sync.md
/// describes for each entity.
@@ -108,7 +113,7 @@ impl Change {
Change {
entity,
id,
op: "delete",
op: DELETE,
edited_at,
..Default::default()
}
@@ -201,10 +206,10 @@ fn collect_deletes(
// Only these exist on the wire; anything else is a bug in a writer, and
// shipping it would earn a rejection that no retry could clear.
let entity: &'static str = match entity.as_str() {
"note" => "note",
"label" => "label",
"attachment" => "attachment",
"preview" => "preview",
entity::NOTE => entity::NOTE,
entity::LABEL => entity::LABEL,
entity::ATTACHMENT => entity::ATTACHMENT,
entity::PREVIEW => entity::PREVIEW,
_ => continue,
};
out.push(Change::delete(entity, id, deleted_at));
@@ -220,9 +225,9 @@ fn collect_labels(conn: &Connection, out: &mut Vec<Change>, limit: usize) -> rus
)?;
let rows = stmt.query_map(params![remaining as i64], |r| {
Ok(Change {
entity: "label",
entity: entity::LABEL,
id: r.get(0)?,
op: "upsert",
op: UPSERT,
name: Some(r.get(1)?),
color: Some(r.get(2)?),
edited_at: r.get(3)?,
@@ -313,15 +318,15 @@ fn note_change(conn: &Connection, id: &str, shared_state: bool) -> rusqlite::Res
// pin, archive and place once they have been changed here, to a server that keeps
// them. Trash, reminders and labels are the owner's, and this account's labels
// were never on it.
if row.permission != "owner" {
if row.permission != access::OWNER {
let state_at = row.state_at.filter(|_| shared_state);
let own = state_at.is_some();
return Ok(Change {
entity: "note",
entity: entity::NOTE,
id: id.to_string(),
op: "upsert",
op: UPSERT,
edited_at: row.updated_at,
body: (row.permission == "edit").then_some(row.body),
body: (row.permission == access::EDIT).then_some(row.body),
pinned: own.then_some(row.pinned),
archived: own.then_some(row.archived),
position: own.then_some(row.position),
@@ -341,9 +346,9 @@ fn note_change(conn: &Connection, id: &str, shared_state: bool) -> rusqlite::Res
};
Ok(Change {
entity: "note",
entity: entity::NOTE,
id: id.to_string(),
op: "upsert",
op: UPSERT,
// The local `updated_at` IS the client's edit time, which is what the
// server's last-write-wins comparison runs against.
edited_at: row.updated_at,
@@ -387,7 +392,7 @@ pub fn apply_results(
} else {
summary.applied += 1;
}
if change.op == "delete" {
if change.op == DELETE {
forget_pending_delete(&tx, change)?;
}
}
@@ -403,7 +408,7 @@ pub fn apply_results(
// lose to the same comparison forever — and let the next pull bring
// the server's copy down.
clear_dirty(&tx, change, None)?;
if change.op == "delete" {
if change.op == DELETE {
// Our delete lost to a newer server edit; the note lives on, and
// the pull will restore it locally. Drop the tombstone so we
// don't keep trying to delete a note the user has since edited.
@@ -448,11 +453,11 @@ pub fn apply_results(
fn clear_dirty(conn: &Connection, change: &Change, revision: Option<i64>) -> rusqlite::Result<()> {
// A delete has no local row left to update.
if change.op == "delete" {
if change.op == DELETE {
return Ok(());
}
let table = match change.entity {
"label" => "labels",
entity::LABEL => "labels",
_ => "notes",
};
match revision {
@@ -469,7 +474,7 @@ fn clear_dirty(conn: &Connection, change: &Change, revision: Option<i64>) -> rus
}
fn forget_pending_delete(conn: &Connection, change: &Change) -> rusqlite::Result<()> {
if change.op != "delete" {
if change.op != DELETE {
return Ok(());
}
conn.execute(