Skip to content

feat(core): tenant-scoped PostgresLibraryRepository (Phase 1.b.5a) - #185

Merged
InstaZDLL merged 1 commit into
mainfrom
feat/core-tenant-library-postgres
May 30, 2026
Merged

feat(core): tenant-scoped PostgresLibraryRepository (Phase 1.b.5a)#185
InstaZDLL merged 1 commit into
mainfrom
feat/core-tenant-library-postgres

Conversation

@InstaZDLL

@InstaZDLL InstaZDLL commented May 30, 2026

Copy link
Copy Markdown
Owner

Summary

Phase 1.b.5a — give `waveflow-server` the tenant-scoped surface for the second resource family (library). Same shape as PRs #181 / #183 / #184 for profile.

Strategy

`PostgresLibraryRepository` carries inherent methods only — does not implement `LibraryRepository`. The trait is single-tenant (no `user_id` parameter); exposing it on a multi-tenant Postgres backend would let a careless caller bypass the user_id filter.

Every method takes both `profile_id` (the owning profile) AND `user_id` (the authenticated user). The SQL encodes the ownership pair (`library.profile_id` ↔ `profile.user_id`) in its WHERE clause, so storage is the single point of enforcement — no convention-by-comment.

Method Pattern
`list_for_profile` `WHERE profile_id = $1 AND EXISTS (… p.user_id = $2)` — empty list for non-owner
`get_for_profile` same `EXISTS`, `Option`
`insert_for_profile` `INSERT … SELECT FROM profile WHERE id = $1 AND user_id = $7 RETURNING *`
`update_for_profile` `UPDATE … COALESCE … RETURNING *` (same race-free pattern as PR #184's rename)
`delete_for_profile` `DELETE … WHERE … AND EXISTS (…)`, `Ok(bool)` for "row removed?"

Domain

`Library` gains `profile_id: i64` with `#[sqlx(default)]` — mirrors how `Profile.user_id` round-trips on the desktop SQLite that has no column for it. The lone desktop call site (`commands/library::create_library`) sets `profile_id: 0` explicitly.

Counts (`track_count`, `album_count`, …) are stubbed at `0::bigint` in every SELECT — they become real aggregates as tracks / playlists land in 1.b.5b+, without changing the wire shape.

Out of scope

  • The `library` schema migration — lives in `waveflow-server/migrations/` (next PR 1.b.5a-PR-B).
  • Folder management methods (`list_folders`, `insert_folder`, etc.) — the desktop's scan path uses them; the server doesn't need them yet.
  • Track / playlist Postgres repositories — 1.b.5b / 1.b.5c.

Test plan

  • `cargo check --workspace --all-targets` ✅
  • `cargo clippy --workspace --all-targets -- -D warnings` ✅
  • `cargo test --workspace` ✅ (66 + 45 = 111 tests pass, zero regression)
  • PR B on waveflow-server consumes this via git dep and exercises every `*_for_profile` method through the `/api/v1/profiles/{profile_id}/libraries` CRUD endpoints + Postgres service container.

Summary by CodeRabbit

Chores

  • Améliorations internes de l'infrastructure de gestion des bibliothèques et support PostgreSQL.

Review Change Stack

Same shape as the profile work in PRs #181 / #183 / #184: server-only
inherent methods that scope every query to both `profile_id` (the
resource's owning profile) and `user_id` (the request's authenticated
user). The single-tenant `LibraryRepository` trait stays untouched on
the desktop side, and `PostgresLibraryRepository` deliberately does
NOT implement it — a careless `Box<dyn LibraryRepository>` over the
Postgres backend would otherwise let user A walk user B's libraries.

Methods (5):
- `list_for_profile(profile_id, user_id)` — MRU-first, empty list
  when the user doesn't own the profile (no tenancy leak, no auth
  pre-check round-trip)
- `get_for_profile(id, profile_id, user_id)` — single row, `None`
  blurs missing / foreign-profile / foreign-user
- `insert_for_profile(draft, profile_id, user_id)` — `INSERT ...
  SELECT FROM profile WHERE id = $1 AND user_id = $7`, returns the
  inserted row via `RETURNING *` so the caller skips a follow-up
  SELECT (same race elimination as PR #184's rename_for_user)
- `update_for_profile(id, patch, now_ms, profile_id, user_id)` —
  COALESCE partial update, `UPDATE ... RETURNING *` for the same
  reason
- `delete_for_profile(id, profile_id, user_id)` — `EXISTS` clause on
  profile validates ownership without a separate join

Every SQL statement encodes the (profile_id, user_id) ownership pair
in its WHERE clause so the storage layer is the single point of
enforcement — no convention-by-comment, no handler-discipline gap.

Domain:
- `Library` gains `profile_id: i64` with `#[sqlx(default)]`, mirroring
  `Profile.user_id`. Desktop SELECTs that omit the column (no
  `profile_id` on the per-profile SQLite `library` table) still
  round-trip cleanly via the default. The lone desktop call site
  (`commands/library::create_library`) now sets `profile_id: 0`
  explicitly to match.

Counts (`track_count`, `album_count`, `artist_count`, `genre_count`,
`folder_count`) are stubbed at `0::bigint` in every SELECT for this
phase; they become real aggregates as tracks / albums / playlists
land in 1.b.5b+, without changing the wire shape.

Schema lives in `waveflow-server/migrations/` (next PR):
`library.profile_id BIGINT NOT NULL REFERENCES profile(id) ON
DELETE CASCADE` + the usual indices.

Zero behaviour change on the desktop. Validated: workspace check +
clippy + 111 tests pass.
@InstaZDLL InstaZDLL added scope: backend Rust/Tauri backend (src-tauri/) type: feat New feature labels May 30, 2026
@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Ce PR ajoute un modèle de tenancy multi-tenant à la structure Library. Le champ profile_id est ajouté à la DTO, un dépôt PostgreSQL complet avec sécurité tenant-scoped (validant profile_id et user_id dans chaque opération SQL) est implémenté, et la commande single-tenant est mise à jour pour définir la sentinelle profile_id: 0.

Changes

Tenancy Model et Repository Postgres Tenant-Scoped

Layer / File(s) Summary
Domain model with profile_id field
src-tauri/crates/core/src/domain/library.rs
Library DTO ajoute profile_id: i64 documenté pour SQLite (0) et Postgres (> 0), avec #[sqlx(default)] conditionnels pour mapper les requêtes sans sélection explicite.
PostgreSQL tenant-scoped repository with CRUD operations
src-tauri/crates/core/src/repository/postgres/library.rs, src-tauri/crates/core/src/repository/postgres/mod.rs
PostgresLibraryRepository implémente list_for_profile, get_for_profile, insert_for_profile, update_for_profile, delete_for_profile avec contrainte tenant profile_id + user_id appliquée via EXISTS dans chaque requête SQL. Les compteurs sont projetés à 0. Module exports ajoutés pour PostgresLibraryRepository.
Command layer single-tenant sentinel
src-tauri/crates/app/src/commands/library.rs
create_library construit explicitement le retour Library avec profile_id: 0, documentant le modèle single-tenant et la cohérence du round-trip.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • InstaZDLL/WaveFlow#181: Modifie Library pour SQLx en Postgres (dérivation FromRow dans le PR référencé vs. ajout du champ profile_id persisté dans ce PR), dépendance DTO directe.

Suggested labels

type: feat, size: xl, scope: backend

Poem

🏢 Les profils s'enferment dans leur profile_id
Postgres valide chaque accès tenant
Single ou multi, la sentinelle guide le flux
Compteurs à zéro, sécurité garantie
WaveFlow prend forme, ligne par ligne ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Le titre suit la convention Conventional Commits avec le scope « core » et décrit précisément l'ajout du PostgresLibraryRepository tenant-scoped, correspondant au changement principal du PR.
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.
Description check ✅ Passed La description respecte la structure du template : titre en Conventional Commits, résumé clair, stratégie détaillée avec table des patterns SQL, domaine expliqué, périmètre délimité, et plan de test complet.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/core-tenant-library-postgres

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

@InstaZDLL InstaZDLL added the size: l 200-500 lines label May 30, 2026
@InstaZDLL InstaZDLL self-assigned this May 30, 2026
@InstaZDLL
InstaZDLL merged commit 0473647 into main May 30, 2026
14 checks passed
@InstaZDLL
InstaZDLL deleted the feat/core-tenant-library-postgres branch May 30, 2026 11:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: backend Rust/Tauri backend (src-tauri/) size: l 200-500 lines type: feat New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant