Skip to content

feat(sync): rfc-003 phase b.2 — entity fetch-by-canonical endpoint - #66

Merged
InstaZDLL merged 2 commits into
mainfrom
feat/sync-v2-phase-b-2-entity-fetch-endpoint
Jun 16, 2026
Merged

feat(sync): rfc-003 phase b.2 — entity fetch-by-canonical endpoint#66
InstaZDLL merged 2 commits into
mainfrom
feat/sync-v2-phase-b-2-entity-fetch-endpoint

Conversation

@InstaZDLL

@InstaZDLL InstaZDLL commented Jun 16, 2026

Copy link
Copy Markdown
Owner

What

GET /api/v1/sync/entity?entity=X&canonical_id=Y[&profile_canonical_id=Z] returns the FULL canonical-fields state of a single materialised row. Counterpart of the digest endpoint: where /sync/digest hands back a hashed snapshot of an entity set, this one serves one row at a time keyed on its canonical id.

The desktop's backfill orchestrator (RFC-003 Phase B.2, ship in a follow-up cross-repo PR) consumes this to resolve digest diffs:

  • missing_locally (server has, desktop doesn't) → fetch + apply locally
  • divergent (same canonical, hashes differ) → fetch remote, compare §2 HLC tuples, apply the LWW winner

Per-entity contracts

Entity Canonical id Source tables fields shape
library UUID library 4 keys (mirror apply::library::canonical_fields)
playlist UUID playlist 4 keys (mirror apply::playlist::canonical_fields)
track <lib_canonical>\u{1F}<file_path> composite track + library + album + artist + track_artist 18 keys (mirror apply::track::canonical_fields)
liked_track file_hash user_liked_track empty {} (binary state)
track_rating file_hash user_track_rating 1 key {rating: i64}

Track sub-selects on track_artist so the multi-artist array matches the apply-side ARRAY_AGG(ar.name ORDER BY ta.position) shape byte-exact — the desktop can recompute payload_hash against the returned (fields, hlc, origin_device_id) to verify the server hasn't drifted from its own write path.

Scope discipline (mirrors /sync/digest)

  • library / playlist / track require profile_canonical_id. Missing → 400.
  • liked_track / track_rating reject profile_canonical_id. Present → 400.
  • Unknown entities → 400.
  • Track composite missing the \u{001F} separator → 400 before the profile resolve so a structural payload bug isn't masked as 404 ("we just don't have that row").

Other responses

  • 404 — profile_canonical_id not visible to this user, or the row exists but its payload_hash is still NULL (pre-B.0 stamp). Both cases indistinguishable on purpose, mirroring the digest endpoint's "invisible until stamped" set membership.
  • 401 — missing or invalid bearer.
  • 500 — DB or internal failure.

What this does NOT do

  • ❌ Does NOT include playlist_track (no canonical_fields / payload_hash server-side per B.0-rest rationale).
  • ❌ Does NOT include profile (the profile row itself doesn't need a per-row fetch — its canonical fields live in the digest sweep).
  • ❌ Does NOT consume the endpoint — that's the desktop PR (Phase B.2 client + apply pipeline) coming next.

New helpers

  • `db::entity_read::{resolve_profile_id, fetch_library, fetch_playlist, fetch_track, fetch_liked, fetch_rating}` — read path counterpart of `digest_read`.
  • `cstr / copt_str / ci64 / copt_i64 / cbool / cstrings` — inline canonical-field inserters. Mirror byte-exact of `apply::canon::*` (and the desktop's `waveflow_core::sync::canon`). Kept local so the pre-B.0a `waveflow-core` pin doesn't need to move just for the same 5 1-liners — a future PR bumps the pin and these switch to the imported helpers without changing behaviour.

Test plan

  • `cargo check --all-targets --all-features` clean
  • `cargo clippy --all-targets --all-features -- -D warnings` clean
  • `cargo test --all-features --test apply_entity` → 10/10 passing
  • `cargo test --all-features --test apply_digest --test apply` → adjacent suites unaffected (12/12 passing)
  • Manual smoke: invoke from a curl against a running dev server.

Tests cover:

  • 5 per-entity round-trips (push → fetch → verify fields shape + payload_hash hex length)
  • Scope discipline matrix (3 cases)
  • Track composite parsing (success + missing separator → 400)
  • 404 paths (unknown canonical_id, cross-tenant isolation)

Refs

  • RFC-003 §4 (backfill protocol)
  • Desktop PR #249 (B.1 digest client + diff — this endpoint resolves what B.1's diff identifies)

Signed-off-by: InstaZDLL claude.ai.cm9ni@twiceland.world

Summary by CodeRabbit

Notes de version

  • Nouvelles fonctionnalités

    • Ajout d'un nouvel endpoint de synchronisation multi-appareils permettant de récupérer l'état complet d'entités (bibliothèques, playlists, pistes et évaluations).
  • Tests

    • Suite complète de tests d'intégration validant la synchronisation et la cohérence des données entre appareils.

`GET /api/v1/sync/entity?entity=X&canonical_id=Y[&profile_canonical_id=Z]`
returns the FULL canonical-fields state of a single materialised row,
so the desktop's backfill orchestrator (RFC-003 Phase B.2) can apply
or merge it under §2 LWW. Counterpart of the digest endpoint: where
digest hands back a hashed snapshot of an entity set, this one
serves one row at a time keyed on its canonical id.

## Per-entity contracts

| Entity         | Canonical id                                | Source tables                       | `fields` shape |
|----------------|---------------------------------------------|-------------------------------------|----------------|
| `library`      | UUID                                        | `library`                           | 4 keys (mirror `apply::library::canonical_fields`) |
| `playlist`     | UUID                                        | `playlist`                          | 4 keys (mirror `apply::playlist::canonical_fields`) |
| `track`        | `<lib_canonical>\u{1F}<file_path>` composite | `track + library + album + artist + track_artist` | 18 keys (mirror `apply::track::canonical_fields`) |
| `liked_track`  | `file_hash`                                 | `user_liked_track`                  | empty `{}` (binary state) |
| `track_rating` | `file_hash`                                 | `user_track_rating`                 | 1 key `{rating: i64}` |

Track sub-selects on `track_artist` so the multi-artist array
matches the apply-side `ARRAY_AGG(ar.name ORDER BY ta.position)`
shape byte-exact, which the desktop can then feed back through
`compute_payload_hash` to verify against the returned `payload_hash`.

## Scope discipline

Same shape as `GET /api/v1/sync/digest`:

- `library` / `playlist` / `track` require `profile_canonical_id`.
- `liked_track` / `track_rating` reject `profile_canonical_id` (the
  endpoint returns 400 if it's present).
- Unknown entities → 400.
- Track composite missing the `\u{001F}` separator → 400 BEFORE the
  profile resolve so a structural payload bug isn't masked as 404
  ("we just don't have that row").

## Other responses

- 404 — `profile_canonical_id` not visible to this user, or the
  row exists but its `payload_hash` is still NULL (pre-B.0 stamp).
  Both cases are indistinguishable on purpose, mirroring the digest
  endpoint's "invisible until stamped" set membership.
- 401 — missing or invalid bearer.
- 500 — DB or internal failure.

## New helpers under `db::entity_read`

- `resolve_profile_id` re-exports the digest module's tenancy
  resolver so the API dispatcher doesn't import both submodules
  for the same SELECT.
- 5 per-entity `fetch_*` functions, each returning
  `Option<EntityFetchResponse>`. Filters mirror `digest_read`:
  `payload_hash IS NOT NULL` (and `canonical_id IS NOT NULL` for
  profile-scoped) keep the visible set consistent with the digest
  sweep.
- `cstr` / `copt_str` / `ci64` / `copt_i64` / `cbool` / `cstrings`
  inline canonical-field inserters. Mirror byte-exact of
  `apply::canon::*` (and the desktop's
  `waveflow_core::sync::canon`). Kept local so the existing
  pre-B.0a `waveflow-core` pin doesn't need to move just for the
  same 5 1-liners — a future PR bumps the pin and these can switch
  to the imported helpers without changing behaviour.

## Tests

10 integration tests in `tests/apply_entity.rs`:

- 5 per-entity round-trip tests (push → fetch → verify
  `fields` + `payload_hash` shape).
- Scope discipline matrix (3 cases).
- Track-specific edge cases (composite parsing, missing separator
  returns 400).
- 404 paths (unknown canonical_id, cross-tenant isolation).

All 10 pass against the `--all-features` integration harness.
`cargo clippy --all-targets --all-features -- -D warnings` clean.

Refs: RFC-003 §4 (backfill protocol), desktop PR #249 (B.1 digest
client + diff — this endpoint is what the diff's `missing_locally`
+ `divergent` outcomes will resolve through)

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@InstaZDLL, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 1 hour, 52 minutes, and 45 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: cf815510-13fc-45bf-8f39-be2e4d920bcd

📥 Commits

Reviewing files that changed from the base of the PR and between d53bbc9 and e685fa6.

📒 Files selected for processing (3)
  • src/api/sync.rs
  • src/db.rs
  • tests/apply_entity.rs
📝 Walkthrough

Walkthrough

Ajout de l'endpoint GET /api/v1/sync/entity permettant de récupérer l'état canonique complet d'une entité par couple (entity, canonical_id). Le changement introduit EntityFetchResponse, un module DB entity_read avec des fetchers par type d'entité, un handler HTTP avec validation de scope et de canonical composite, et une suite de tests d'intégration.

Changes

Endpoint GET /api/v1/sync/entity – read-path complet

Layer / File(s) Résumé
Contrat de réponse EntityFetchResponse
src/sync.rs
Nouvelle structure publique EntityFetchResponse avec entity, canonical_id, payload_hash, hlc, origin_device_id, fields, et les champs optionnels library_canonical_id / file_path pour track, annotés Serde/OpenAPI.
Module entity_read : fetch par entité et assemblage
src/db.rs
Nouveau pub mod entity_read : helpers de construction de Map JSON, resolve_profile_id, fetch_library, fetch_playlist, fetch_track (jointures album/artist, canonical composite U+001F), fetch_liked, fetch_rating, et build_response centralisant l'encodage hex du payload_hash et le mapping HLC.
Handler get_entity et routage OpenAPI
src/api/sync.rs
Ajout de EntityFetchQuery, enregistrement de la route GET dans router(), et handler get_entity : validation des paramètres, séparateur U+001F pour track, résolution de scope profil/utilisateur, dispatch vers entity_read, réponses 200/400/404/500.
Tests d'intégration round-trip et isolation
tests/apply_entity.rs
Round-trips pour library, playlist, track, liked_track, track_rating ; 400 sur canonical composite incomplet ; validation de scope (400 dans les deux sens) ; 404 sur canonical inconnu ; isolation cross-tenant (404 via profile_canonical_id d'un autre utilisateur).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • InstaZDLL/waveflow-server#55 : La PR #55 stamps hlc_wall, hlc_logical et origin_device_id lors de l'apply_op ; ce PR lit et expose exactement ces colonnes via EntityFetchResponse.
  • InstaZDLL/waveflow-server#26 : La PR #26 matérialise les entités côté serveur et propage profile_canonical_id/canonical_id dans la DB ; ce PR consomme ces données persistées pour implémenter GET /api/v1/sync/entity.
  • InstaZDLL/waveflow-server#51 : La PR #51 introduit les colonnes payload_hash, hlc_wall, hlc_logical et origin_device_id que ce PR lit dans entity_read et expose via EntityFetchResponse.

Suggested labels

size: l

Poem

🎵 Un GET pour les gouverner tous,
canonical_id dans la requête, scope résolu sans courroux.
Le U+001F sépare la librairie du chemin,
payload_hash en hex, hlc bien en main —
404 ou 200, chaque entité trouve son destin ! 🎶

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning Description couvre changements substantiels, contrats, scope et cas d'erreur. Format bun/CI manquants. Ajouter sections manquantes sur format bun et intégration CI requises.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Le titre décrit précisément le changement principal : implémentation de RFC-003 Phase B.2 avec nouvel endpoint.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sync-v2-phase-b-2-entity-fetch-endpoint

Comment @coderabbitai help to get the list of available commands and usage tips.

@InstaZDLL InstaZDLL self-assigned this Jun 16, 2026
@github-actions github-actions Bot added scope: server Server core (Rust) scope: db SQLite schema, migrations, queries scope: api Native /api/v2 surface scope: sync User-data sync type: feat New feature size: xl > 500 lines and removed type: feat New feature labels Jun 16, 2026
@InstaZDLL

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions github-actions Bot added type: feat New feature and removed type: feat New feature labels Jun 16, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/api/sync.rs`:
- Around line 716-746: The docstring for the `get_entity` handler function is
unclear about entity coverage. Currently it states "Same scope discipline as
[`get_digest`]" which only describes the `profile_canonical_id` requirement
rules, not which entities are actually supported. Update the docstring to
explicitly clarify that unlike `/digest`, the `/entity` endpoint does not
support the `profile` entity. List the actually supported entities (`library`,
`playlist`, `track`, `liked_track`, `track_rating`) in the documentation and
make clear that `profile` is intentionally excluded by design.

In `@src/db.rs`:
- Around line 1528-1532: The Rust code in multiple files fails the cargo fmt
formatting checks. Run `cargo fmt --all` from the project root to automatically
fix all formatting violations across the codebase. This will fix formatting
issues in the copt_str function at src/db.rs lines 1528-1532, the
album_artist_name invocation at src/db.rs lines 1721-1727, and the fetch
function signature at tests/apply_entity.rs lines 67-79. All three locations
should be corrected by this single formatting command.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c99f3a35-f108-4060-941c-b0c29341d20d

📥 Commits

Reviewing files that changed from the base of the PR and between ffa912e and d53bbc9.

📒 Files selected for processing (4)
  • src/api/sync.rs
  • src/db.rs
  • src/sync.rs
  • tests/apply_entity.rs

Comment thread src/api/sync.rs
Comment thread src/db.rs
Comment on lines +1528 to +1532
fn copt_str(m: &mut Map<String, Value>, k: &str, v: Option<&str>) {
m.insert(
k.to_owned(),
v.map(|s| Value::String(s.to_owned())).unwrap_or(Value::Null),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Échecs de formatage Rust (cargo fmt) dans plusieurs fichiers.

Le pipeline CI échoue car le code n'est pas formaté selon les conventions Rust. Exécute cargo fmt --all pour corriger tous les fichiers d'un coup.

  • src/db.rs#L1528-L1532 : formatage de la fonction copt_str (.unwrap_or(Value::Null)).
  • src/db.rs#L1721-L1727 : formatage de l'invocation multi-ligne pour album_artist_name.
  • tests/apply_entity.rs#L67-L79 : formatage de la signature de la fonction fetch.
🧰 Tools
🪛 GitHub Actions: CI (Rust) / 0_Rust (ubuntu-latest).txt

[error] 1528-1528: cargo fmt --all --check failed due to rustfmt formatting differences in this file (string mapping/unwrap_or formatting). Run 'cargo fmt --all' to apply formatting.

🪛 GitHub Actions: CI (Rust) / Rust (ubuntu-latest)

[error] 1528-1528: cargo fmt --all --check failed (formatting diff). Please run cargo fmt --all to apply Rust formatting changes.

📍 Affects 2 files
  • src/db.rs#L1528-L1532 (this comment)
  • src/db.rs#L1721-L1727
  • tests/apply_entity.rs#L67-L79
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/db.rs` around lines 1528 - 1532, The Rust code in multiple files fails
the cargo fmt formatting checks. Run `cargo fmt --all` from the project root to
automatically fix all formatting violations across the codebase. This will fix
formatting issues in the copt_str function at src/db.rs lines 1528-1532, the
album_artist_name invocation at src/db.rs lines 1721-1727, and the fetch
function signature at tests/apply_entity.rs lines 67-79. All three locations
should be corrected by this single formatting command.

Source: Pipeline failures

Two CodeRabbit findings on PR #66:

1. `get_entity` docstring under-documented entity coverage — said
   "same scope discipline as /digest" without flagging that the
   actual entity set diverges. /digest serves `profile` so the
   desktop can verify the auto-provisioned row's canonical
   fields; /entity has no `profile` reader because the canonical
   id used to address it (`profile_canonical_id`) IS the per-
   tenant scope identifier — a round-trip on the same id, with
   the row's HLC + payload_hash already observable from /digest.
   Rewrote the docstring to enumerate the 5 supported entities
   + an explicit "intentionally excluded: profile" block with
   the rationale.

2. `cargo fmt --all --check` flagged 3 sites the manual edits
   left unindented (the `copt_str` `unwrap_or` chain, the
   `album_artist_name` invocation, the `fetch` test helper
   signature). Ran `cargo fmt --all`.

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
@github-actions github-actions Bot added type: feat New feature and removed type: feat New feature labels Jun 16, 2026
@InstaZDLL
InstaZDLL merged commit b9b35d9 into main Jun 16, 2026
10 checks passed
@InstaZDLL
InstaZDLL deleted the feat/sync-v2-phase-b-2-entity-fetch-endpoint branch June 16, 2026 01:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: api Native /api/v2 surface scope: db SQLite schema, migrations, queries scope: server Server core (Rust) scope: sync User-data sync size: xl > 500 lines type: feat New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant