Compare commits

...
Author SHA1 Message Date
Elliot Slusky 76f077e12a fix(connectors): sync new Apple Notes
Fixes #719.
2026-08-14 17:51:41 -07:00
3 changed files with 110 additions and 22 deletions
@@ -422,12 +422,17 @@ We recommend creating **one Slack app** that handles both. The App Manifest belo
2. Apple Notes is detected automatically when Full Disk Access is granted
OpenJarvis searches an indexed snapshot rather than querying Notes.app live.
After creating notes, open **Data Sources** and click **Re-sync** on Apple Notes
before searching for the new content.
### Troubleshooting
| Issue | Solution |
|-------|----------|
| "Not connected" despite Full Disk Access | Restart your terminal app after granting access |
| Notes content is garbled | Some very old notes may have encoding issues. Most notes should be clean. |
| New notes are missing | In **Data Sources**, click **Re-sync** on Apple Notes to refresh the index. |
| Missing notes | Only notes stored locally or in iCloud are indexed. Notes in third-party accounts (Gmail, Exchange) may not appear. |
---
+42 -20
View File
@@ -9,8 +9,9 @@ System Settings → Privacy & Security → Full Disk Access.
Timestamp notes
---------------
The Notes database stores modification timestamps as seconds since the Apple
epoch of 2001-01-01 00:00:00 UTC. Conversion formula::
Modern Notes schemas store note modification timestamps in
``ZMODIFICATIONDATE1``; older schemas use ``ZMODIFICATIONDATE``. Both are
seconds since the Apple epoch of 2001-01-01 00:00:00 UTC. Conversion formula::
dt = datetime(2001, 1, 1, tzinfo=utc) + timedelta(seconds=ZMODIFICATIONDATE)
@@ -171,25 +172,46 @@ class AppleNotesConnector(BaseConnector):
return
try:
try:
rows = conn.execute(
"SELECT n.ZIDENTIFIER, "
" COALESCE(n.ZTITLE1, n.ZTITLE, '') AS title, "
" n.ZMODIFICATIONDATE, d.ZDATA "
"FROM ZICCLOUDSYNCINGOBJECT n "
"JOIN ZICNOTEDATA d ON d.ZNOTE = n.Z_PK "
"ORDER BY n.ZMODIFICATIONDATE ASC"
).fetchall()
except sqlite3.OperationalError:
# Older macOS schemas may lack ZTITLE1
rows = conn.execute(
"SELECT n.ZIDENTIFIER, "
" COALESCE(n.ZTITLE, '') AS title, "
" n.ZMODIFICATIONDATE, d.ZDATA "
"FROM ZICCLOUDSYNCINGOBJECT n "
"JOIN ZICNOTEDATA d ON d.ZNOTE = n.Z_PK "
"ORDER BY n.ZMODIFICATIONDATE ASC"
object_columns = {
row[1]
for row in conn.execute(
"PRAGMA table_info(ZICCLOUDSYNCINGOBJECT)"
).fetchall()
}
title_columns = [
f"n.{column}"
for column in ("ZTITLE1", "ZTITLE")
if column in object_columns
]
title_expr = (
f"COALESCE({', '.join(title_columns)}, '')" if title_columns else "''"
)
# Modern Apple Notes stores a note's modification timestamp in
# ZMODIFICATIONDATE1. ZMODIFICATIONDATE is still present in some
# schemas, but applies to other cloud-sync object types and can be
# NULL for notes. Treating that NULL as zero makes incremental
# syncs incorrectly discard newly-created notes as 2001-era data.
modification_columns = [
f"n.{column}"
for column in ("ZMODIFICATIONDATE1", "ZMODIFICATIONDATE")
if column in object_columns
]
modification_expr = (
f"COALESCE({', '.join(modification_columns)}, 0)"
if modification_columns
else "0"
)
rows = conn.execute(
"SELECT n.ZIDENTIFIER, "
f" {title_expr} AS title, "
f" {modification_expr} AS modification_date, d.ZDATA "
"FROM ZICCLOUDSYNCINGOBJECT n "
"JOIN ZICNOTEDATA d ON d.ZNOTE = n.Z_PK "
"ORDER BY modification_date ASC"
).fetchall()
self._items_total = len(rows)
synced = 0
+63 -2
View File
@@ -8,6 +8,7 @@ from __future__ import annotations
import gzip
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
from typing import List
@@ -24,7 +25,8 @@ from openjarvis.core.registry import ConnectorRegistry
def _create_fake_notes_db(db_path: Path) -> None:
"""Populate a SQLite file with the Apple Notes schema and sample rows."""
conn = sqlite3.connect(str(db_path))
conn.executescript("""
conn.executescript(
"""
CREATE TABLE ZICCLOUDSYNCINGOBJECT (
Z_PK INTEGER PRIMARY KEY,
ZTITLE TEXT,
@@ -38,7 +40,8 @@ def _create_fake_notes_db(db_path: Path) -> None:
ZDATA BLOB,
ZNOTE INTEGER
);
""")
"""
)
# Note 1 — Shopping List
html1 = "<html><body><h1>Shopping List</h1><p>Milk, eggs, bread</p></body></html>"
@@ -208,3 +211,61 @@ def test_registry() -> None:
assert ConnectorRegistry.contains("apple_notes")
cls = ConnectorRegistry.get("apple_notes")
assert cls.connector_id == "apple_notes"
# ---------------------------------------------------------------------------
# Test 10 — modern modification timestamp drives incremental sync
# ---------------------------------------------------------------------------
def test_incremental_sync_uses_modern_note_modification_date(tmp_path: Path) -> None:
"""Modern Notes rows use ZMODIFICATIONDATE1 for incremental sync."""
db_path = tmp_path / "ModernNoteStore.sqlite"
conn = sqlite3.connect(str(db_path))
conn.executescript(
"""
CREATE TABLE ZICCLOUDSYNCINGOBJECT (
Z_PK INTEGER PRIMARY KEY,
ZTITLE TEXT,
ZTITLE1 TEXT,
ZMODIFICATIONDATE REAL,
ZMODIFICATIONDATE1 REAL,
ZIDENTIFIER TEXT
);
CREATE TABLE ZICNOTEDATA (
Z_PK INTEGER PRIMARY KEY,
ZDATA BLOB,
ZNOTE INTEGER
);
"""
)
compressed = gzip.compress(b"<p>New movie list</p>")
conn.execute(
"INSERT INTO ZICCLOUDSYNCINGOBJECT VALUES "
"(1, NULL, 'Movies', NULL, 800000000.0, 'note-modern')"
)
conn.execute("INSERT INTO ZICNOTEDATA VALUES (1, ?, 1)", (compressed,))
conn.commit()
conn.close()
from openjarvis.connectors.apple_notes import AppleNotesConnector # noqa: PLC0415
connector = AppleNotesConnector(db_path=str(db_path))
docs = list(connector.sync(since=datetime(2026, 1, 1, tzinfo=timezone.utc)))
assert [doc.doc_id for doc in docs] == ["apple_notes:note-modern"]
# ---------------------------------------------------------------------------
# Test 11 — legacy modification timestamp remains supported
# ---------------------------------------------------------------------------
def test_incremental_sync_falls_back_to_legacy_modification_date(connector) -> None:
"""Older Notes rows continue to use ZMODIFICATIONDATE."""
docs = list(connector.sync(since=datetime(2023, 1, 1, tzinfo=timezone.utc)))
assert {doc.doc_id for doc in docs} == {
"apple_notes:note-001",
"apple_notes:note-002",
}