Skip to content

feat(library): tenant-scoped CRUD nested under profile (Phase 1.b.5a-B)#8

Merged
InstaZDLL merged 3 commits into
mainfrom
feat/library-crud-1-b-5a-b
May 30, 2026
Merged

feat(library): tenant-scoped CRUD nested under profile (Phase 1.b.5a-B)#8
InstaZDLL merged 3 commits into
mainfrom
feat/library-crud-1-b-5a-b

Conversation

@InstaZDLL
Copy link
Copy Markdown
Owner

@InstaZDLL InstaZDLL commented May 30, 2026

Summary

Wires the new PostgresLibraryRepository (from waveflow#185, merged at 04736478) to a nested HTTP resource at /api/v1/profiles/{profile_id}/libraries/*. Same tenancy contract as profiles, extended one level deeper:

  • Path supplies profile_id, middleware supplies UserId.
  • Repo SQL validates library → profile → user ownership inline — a foreign profile id or non-owned library 404s without ever loading the row.
  • INSERT … SELECT … WHERE p.user_id = $ and UPDATE … RETURNING * eliminate the check-then-write / update-then-read races.

Changes

  • Migration 20260530000002_library.sqllibrary table with id BIGSERIAL, profile_id BIGINT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, name/description/color_id/icon_id, created_at/updated_at. Composite index (profile_id, updated_at DESC) for the per-tenant MRU list; covers the FK fan-out so no second profile_id-only index.
  • src/api/libraries.rs — 5 verbs (list/create/get/update/delete) with full #[utoipa::path] annotations including 500 responses. Wire format drops profile_id since the client always has it from the URL.
  • src/api/mod.rslibraries_router gated identically to profiles: 503 in production, require_user_id when WAVEFLOW_DEV_AUTH=1.
  • tests/libraries.rs — 9 integration tests covering: 401 auth gate, default color/icon fall-back, create-list-get round-trip, 404 on foreign profile, tenant isolation (user B can't see, GET, PATCH or DELETE user A's library even when proxying through user A's profile id), partial PATCH preservation, delete returns 204 then 404, ON DELETE CASCADE from profile, and the production 503 gate.
  • tests/ready.rs + tests/openapi.rs — Canary on the library table existence + path-presence assertions on the new OpenAPI routes.
  • Cargo.toml — bumps waveflow-core rev to 04736478 (#185 merge commit).

Test plan

  • cargo check --all-targets
  • cargo fmt --all --check
  • cargo clippy --all-targets -- -D warnings
  • cargo test --all (CI exercises this against a Postgres service container — local Postgres isn't always available)

Refs: waveflow#185, RFC-001 §6.5.

Summary by CodeRabbit

  • New Features
    • API CRUD pour gérer des bibliothèques par profil (nom, description, couleur, icône), tri par mise à jour et isolation multi‑tenant; suppression en cascade avec le profil.
  • Documentation
    • Routes OpenAPI ajoutées pour les endpoints de bibliothèques.
  • Tests
    • Suite e2e et tests de migration vérifiant création, lecture, mise à jour, suppression, validations, isolation multi‑tenant et disponibilité des routes.
  • Chores
    • Mise à jour d’une dépendance interne.

Review Change Stack

Wires `PostgresLibraryRepository` (introduced in waveflow#185) to a new
`/api/v1/profiles/{profile_id}/libraries/*` resource. Same tenancy
pattern as profiles: every handler reads `UserId` from the request
extension, the path supplies `profile_id`, and the repo SQL validates
the `library -> profile -> user` chain inline. A foreign profile id
or non-owned library 404s — no existence leak.

- New migration `20260530000002_library.sql` (BIGSERIAL pk, profile_id
  FK with ON DELETE CASCADE, composite index on profile_id + updated_at).
- New `src/api/libraries.rs` with list/create/get/update/delete + full
  OpenAPI annotations including 500s.
- Wired in `src/api/mod.rs` behind the same dev-auth gate as profiles:
  503 in prod, `require_user_id` middleware when WAVEFLOW_DEV_AUTH=1.
- Integration tests cover auth gate, default color/icon fall-back,
  tenant isolation across both list and id-based reads, partial PATCH
  preservation, ON DELETE CASCADE from profile, and the prod 503 gate.
- Belt-and-braces: library table existence canary in `ready.rs` and
  path-presence asserts in `openapi.rs`.
- Bumps waveflow-core rev to the merge commit of waveflow#185.

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

coderabbitai Bot commented May 30, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a16cf086-d803-448a-88f7-e6cf18aa736c

📥 Commits

Reviewing files that changed from the base of the PR and between 0999b7d and c5c2cad.

📒 Files selected for processing (2)
  • src/api/libraries.rs
  • tests/libraries.rs

📝 Walkthrough

Walkthrough

Ce PR ajoute une API REST tenant-scoped /api/v1/profiles/{profile_id}/libraries/* : migration SQL créant la table library, types de requête/réponse, cinq handlers CRUD, intégration au router avec gating dev-auth, OpenAPI et tests end-to-end couvrant sécurité multi-tenant et cascade delete.

Changes

Subsystem de gestion des bibliothèques multi-tenant

Layer / File(s) Summary
Database schema and dependency upgrade
Cargo.toml, migrations/20260530000002_library.sql, tests/ready.rs
Migration SQL crée la table library multi-tenant avec profile_id BIGINT NOT NULL REFERENCES profile(id) ON DELETE CASCADE et index library_profile_updated_idx sur (profile_id, updated_at DESC). Test vérifie la présence de public.library après exécution du migrator. waveflow-core git rev mis à jour.
Router integration and auth gating
src/api/mod.rs
Ajout de mod libraries; et montage du libraries_router dans le routeur principal avec application conditionnelle de require_user_id en dev ou reject_dev_auth_disabled sinon.
API data contracts and response types + handlers
src/api/libraries.rs
Définitions LibraryResponse, CreateLibraryRequest, UpdateLibraryRequest, constantes par défaut, et implémentation des handlers list/create/get/update/delete (trim/validation name, defaults pour color_id/icon_id, utilisation des méthodes *_for_profile, codes 200/201/204/404/500 et journalisation).
End-to-end integration tests
tests/libraries.rs
Tests E2E: rejet 401 sans x-user-id, create→list→get avec valeurs par défaut et explicites, validation name vide (400), isolation multi-tenant (404 pour cross-user), PATCH partiel et complet, suppression (204) et cascade delete via suppression de profile, gating dev-auth (503 en prod gate).
OpenAPI spec assertions
tests/openapi.rs
Ajout d'assertions vérifiant que le spec OpenAPI expose GET /api/v1/profiles/{profile_id}/libraries et GET /api/v1/profiles/{profile_id}/libraries/{id}.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • InstaZDLL/waveflow-server#6: Intégration et patterns de dev-auth/tenant routing partagés avec les endpoints profiles que ce PR étend.
  • InstaZDLL/waveflow-server#4: Mise en place antérieure de l'infrastructure OpenAPI/utoipa sur laquelle s'appuie l'ajout des routes libraries.

Poem

📚 Une table naît, liée au profil,
Les routes chantent CRUD en mode multi-tenant,
Les tests verrouillent 401 et 404,
DELETE cascade, plus de traces fugitives,
L'OpenAPI expose la danse des routes.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Le titre décrit clairement le changement principal : ajout d'une API REST tenant-scoped pour les bibliothèques imbriquées sous les profils, avec les opérations CRUD complètes.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/library-crud-1-b-5a-b

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

@InstaZDLL InstaZDLL self-assigned this May 30, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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/libraries.rs`:
- Around line 181-211: Dans create_library, valider côté serveur que le nom
n'est pas vide après trim: récupère req.name.trim(), si la chaîne résultante est
vide retourne immédiatement une réponse 400 (StatusCode::BAD_REQUEST) avec un
message d'erreur clair (ex. "name is required"), et n'appelle pas
PostgresLibraryRepository::insert_for_profile; construis LibraryDraft.name avec
la version trimée; enfin mets à jour l'annotation #[utoipa::path] pour
documenter la réponse 400 pour ce point d'entrée (create_library /
CreateLibraryRequest).

In `@tests/libraries.rs`:
- Around line 456-502: The test profile_delete_cascades_to_libraries currently
only checks the library via the deleted profile (p1) so rename the unused _p2 to
p2 (so it’s available) and after deleting p1 perform the GET against
/api/v1/profiles/{p2}/libraries/{lib_id} as well, asserting the response status
is StatusCode::NOT_FOUND to verify the library row was actually removed by the
ON DELETE CASCADE; update references to p2 in that added request and keep
existing checks for p1.
🪄 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: 27fb89e5-6856-4390-b62c-b5c657f242c3

📥 Commits

Reviewing files that changed from the base of the PR and between 19c8f78 and 2b3957b.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !Cargo.lock, !*.lock
📒 Files selected for processing (7)
  • Cargo.toml
  • migrations/20260530000002_library.sql
  • src/api/libraries.rs
  • src/api/mod.rs
  • tests/libraries.rs
  • tests/openapi.rs
  • tests/ready.rs

Comment thread src/api/libraries.rs
Comment thread tests/libraries.rs
- create_library: trim req.name and reject empty / whitespace-only
  payloads with 400 before the storage round-trip. Update the
  CreateLibraryRequest doc comment to reflect the new boundary
  validation and document the 400 in the OpenAPI #[utoipa::path]
  responses(...). Persist the trimmed form.
- profile_delete_cascades_to_libraries: promote the previously-unused
  _p2 to p2 and after deleting p1 also GET the library via p2 to
  prove the row is truly gone via CASCADE (not just that the deleted
  profile path no longer resolves).
- Add create_rejects_empty_name integration test covering the new
  400 path across "", "   ", and "\t\n " inputs, plus a follow-up
  list assertion confirming nothing was persisted.

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

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/api/libraries.rs (1)

284-309: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

La validation name manque dans update_library.

create_library rejette les noms vides/whitespace avec 400, mais update_library passe req.name directement au repo sans vérification. Un PATCH { "name": "" } ou { "name": " " } contournerait la validation.

🛡️ Correction proposée
 async fn update_library(
     State(state): State<AppState>,
     Extension(UserId(user_id)): Extension<UserId>,
     Path((profile_id, id)): Path<(i64, i64)>,
     Json(req): Json<UpdateLibraryRequest>,
 ) -> impl IntoResponse {
+    // Same boundary validation as create — reject blank names.
+    let name = match req.name {
+        Some(n) => {
+            let trimmed = n.trim();
+            if trimmed.is_empty() {
+                return (StatusCode::BAD_REQUEST, "name must not be empty").into_response();
+            }
+            Some(trimmed.to_string())
+        }
+        None => None,
+    };
     let patch = LibraryUpdate {
-        name: req.name,
+        name,
         description: req.description,
         color_id: req.color_id,
         icon_id: req.icon_id,
     };

Pense à documenter la réponse 400 dans l'annotation #[utoipa::path] pour update_library également.

🤖 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/api/libraries.rs` around lines 284 - 309, The update_library handler
fails to validate the incoming name allowing empty or whitespace names to pass
into LibraryUpdate and repo.update_for_profile; modify update_library to trim
and validate req.name before constructing LibraryUpdate (reject empty/whitespace
with an early 400 response), mirror the same validation used in create_library,
and ensure the utoipa #[utoipa::path] annotation for update_library documents
the 400 response for invalid name.
tests/libraries.rs (1)

362-401: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Ajouter un test pour la validation name lors du PATCH.

Si la validation dans update_library est ajoutée (cf. commentaire sur src/api/libraries.rs), un test analogue à create_rejects_empty_name serait utile pour couvrir le chemin PATCH { "name": "" } → 400.

🤖 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 `@tests/libraries.rs` around lines 362 - 401, Add a new test similar to
create_rejects_empty_name that exercises update_library by sending a PATCH to
/api/v1/profiles/{profile_id}/libraries/{id} with { "name": "" } and asserting a
400 response; locate the test scaffolding used in update_renames_in_place
(spawn_app, mint_user, mint_profile) to create a library first, then issue the
PATCH and assert error_for_status or explicitly check status==400 and validate
the error body matches the validation error for name.
🤖 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.

Outside diff comments:
In `@src/api/libraries.rs`:
- Around line 284-309: The update_library handler fails to validate the incoming
name allowing empty or whitespace names to pass into LibraryUpdate and
repo.update_for_profile; modify update_library to trim and validate req.name
before constructing LibraryUpdate (reject empty/whitespace with an early 400
response), mirror the same validation used in create_library, and ensure the
utoipa #[utoipa::path] annotation for update_library documents the 400 response
for invalid name.

In `@tests/libraries.rs`:
- Around line 362-401: Add a new test similar to create_rejects_empty_name that
exercises update_library by sending a PATCH to
/api/v1/profiles/{profile_id}/libraries/{id} with { "name": "" } and asserting a
400 response; locate the test scaffolding used in update_renames_in_place
(spawn_app, mint_user, mint_profile) to create a library first, then issue the
PATCH and assert error_for_status or explicitly check status==400 and validate
the error body matches the validation error for name.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 972d3607-9bc1-422f-aafc-6b55e2792475

📥 Commits

Reviewing files that changed from the base of the PR and between 2b3957b and 0999b7d.

📒 Files selected for processing (2)
  • src/api/libraries.rs
  • tests/libraries.rs

Same boundary check as create_library: a PATCH whose `name` is
Some("") / Some("   ") trims to empty and short-circuits to 400
before the storage round-trip. An omitted name (None) stays
legitimate — COALESCE in the repo preserves the existing value.

- OpenAPI annotation for PATCH documents the new 400 response.
- UpdateLibraryRequest doc-comment notes the trim+validate rule.
- update_rejects_empty_name integration test exercises "", "   "
  and "\t\n " through the PATCH path, plus a follow-up GET
  confirming the existing name wasn't blanked.

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
@InstaZDLL
Copy link
Copy Markdown
Owner Author

@coderabbitai bien vu, tu as raison sur les deux outside-diff findings. J'avais sciemment skip update_library sous "scope minimal" mais c'est exactement le même boundary problem — un PATCH { "name": "" } contournait la validation.

Appliqué dans c5c2cad :

  • update_library : trim + early-return 400 name must not be empty quand req.name est Some(trimmed_empty). None reste légitime (COALESCE preserve la valeur existante).
  • Annotation #[utoipa::path] patch documente le 400.
  • Doc-comment de UpdateLibraryRequest réécrit pour refléter la règle trim+validate, miroir de CreateLibraryRequest.
  • Test update_rejects_empty_name couvre "", " ", "\t\n " sur la route PATCH + assertion follow-up via GET que le name d'origine n'a pas été blanké par une des requêtes rejetées.

cargo check --all-targets + fmt --check + clippy -D warnings verts.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 30, 2026

Tip

For best results, initiate chat on the files or code changes.

@InstaZDLL Implémentation correcte — symétrie parfaite avec create_library, None laissé intact pour COALESCE, et l'assertion GET dans update_rejects_empty_name couvre exactement le cas qui m'inquiétait (mutation silencieuse sur payload rejeté). C'est solide.

🐇 ✓

@InstaZDLL InstaZDLL merged commit 57c4832 into main May 30, 2026
8 checks passed
@InstaZDLL InstaZDLL deleted the feat/library-crud-1-b-5a-b branch May 30, 2026 11:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant