feat: add bidirectional tag sync#120
Conversation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DatabaseConnection.transaction() already calls the inner fn — no need for extra () at call site. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 26f2757055
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| const changes = await db | ||
| .select() | ||
| .from(tagSyncLog) |
There was a problem hiding this comment.
Add migration for tag_sync_log before querying it
These handlers read from tag_sync_log, but this commit only adds the Drizzle schema/type and route code; it does not add a corresponding SQL migration entry under packages/api/drizzle (or a new journal entry). In environments that apply schema changes via pnpm db:migrate, the table will not exist, so /sync/tags requests will fail at runtime with a database error (no such table: tag_sync_log).
Useful? React with 👍 / 👎.
| const stmt = this.db.prepare(` | ||
| SELECT id, uuid, name, color, local_version | ||
| FROM tags | ||
| WHERE needs_sync = 1 AND uuid IS NOT NULL |
There was a problem hiding this comment.
Assign UUIDs when creating new local tags
Pending tag sync now requires uuid IS NOT NULL, but local tag creation paths still insert tags without a UUID (existing inserts only provide name). After this change, newly created local tags can be marked needs_sync=1 yet never selected for upload, so tags created on one device will not propagate to other devices.
Useful? React with 👍 / 👎.
| local_version = local_version + 1, | ||
| needs_sync = 1 | ||
| WHERE id = NEW.id; |
There was a problem hiding this comment.
Exclude remote-applied changes from sync-tracking triggers
This trigger marks any tag name/color update as needs_sync=1, including updates applied during pullTags() (upsertTagFromRemote updates these same columns). Because syncNow() then runs pushTags(), pulled remote changes are immediately re-queued for upload, creating echo/conflict loops where tags do not remain in a clean synced state.
Useful? React with 👍 / 👎.
|
|
||
| const changes = pending.map(({ tag, localVersion }) => ({ | ||
| tagId: tag.uuid, | ||
| operation: 'update' as const, |
There was a problem hiding this comment.
Emit delete operations for removed tags
Client-side tag push currently hardcodes every change as operation: 'update'. Since deleted tags are removed from the local tags table before sync, no delete payload can be generated even though the server endpoint supports 'delete', so tag deletions on one device will not be propagated and stale tags remain on other devices.
Useful? React with 👍 / 👎.
# Conflicts: # apps/desktop/src/main/services/apiClient.ts # apps/desktop/src/main/services/syncService.ts # packages/api/src/db/schema.ts # packages/api/src/routes/sync.ts # packages/storage-sqlite/src/migrations/index.ts
Summary
tagSyncLogtable to server schema (Drizzle/Turso)GET /sync/tagsandPOST /sync/tagsAPI endpoints (cursor-based, Pro subscription gated)getTagsPendingSync,markTagAsSynced,upsertTagFromRemote(dedup by UUID then name),deleteTagByUuidpullTagChanges/pushTagChangesto API clientsyncNow()cycle (pull tags → push tags, after note sync)sync:pullTags,sync:pushTags) and preload APIDesign decisions
Test plan
pnpm typecheckpasses (all 3 targets: main, preload, renderer)pnpm lintcleanpnpm format:checkcleanpnpm testpasses (all 14 tasks)🤖 Generated with Claude Code