fix: prevent duplicate folder_ipns row on vault init#351
Conversation
The IPNS publish endpoint creates a folder_ipns row on first publish. When clients publish the root folder IPNS record before calling POST /vault/init, the init endpoint would try to insert a second row for the same (user_id, ipns_name), hitting the unique constraint. Fix: check if the folder_ipns row already exists and update isRoot instead of blindly inserting. This matches the new vault init flow where IPNS records are published before the backend vault registration. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Entire-Checkpoint: a3faa0447497
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
Walkthrough
Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
apps/api/src/vault/vault.service.ts (1)
80-101: Consider atomic upsert to eliminate TOCTOU race window.The check-then-write pattern has a narrow race window: if a concurrent request (e.g., the publish endpoint) inserts a row between
findOnereturning null andsaveexecuting, the unique constraint violation will bubble up as an unhandled 500 error.An atomic upsert using TypeORM's
orUpdatewould be more robust and follows the pattern already used byrecordPin(line 183):♻️ Suggested atomic upsert
- const existingFolderIpns = await this.folderIpnsRepository.findOne({ - where: { userId, ipnsName: dto.rootIpnsName }, - }); - - if (existingFolderIpns) { - // Mark as root if not already (publish endpoint creates with isRoot=false) - if (!existingFolderIpns.isRoot) { - existingFolderIpns.isRoot = true; - await this.folderIpnsRepository.save(existingFolderIpns); - } - } else { - const rootFolderIpns = this.folderIpnsRepository.create({ - userId, - ipnsName: dto.rootIpnsName, - latestCid: null, - sequenceNumber: '0', - encryptedIpnsPrivateKey: null, - keyEpoch: null, - isRoot: true, - }); - await this.folderIpnsRepository.save(rootFolderIpns); - } + await this.folderIpnsRepository + .createQueryBuilder() + .insert() + .into(FolderIpns) + .values({ + userId, + ipnsName: dto.rootIpnsName, + latestCid: null, + sequenceNumber: '0', + encryptedIpnsPrivateKey: null, + keyEpoch: null, + isRoot: true, + }) + .orUpdate(['is_root'], ['user_id', 'ipns_name']) + .execute();This atomically inserts a new row or updates
is_root=trueon conflict, eliminating the race window entirely.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/api/src/vault/vault.service.ts` around lines 80 - 101, The current check-then-write using folderIpnsRepository.findOne followed by save has a TOCTOU race; replace it with an atomic upsert like the pattern used in recordPin so the insert-or-update happens in one DB statement: use the repository.create(...) or repository.createQueryBuilder() and call orUpdate (or the equivalent TypeORM upsert) to insert the row with isRoot=true or update the existing row to set isRoot=true on conflict, removing the findOne/save flow (remove existingFolderIpns and rootFolderIpns branching) and ensure the unique key (ipnsName+userId) is used as the conflict target.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@apps/api/src/vault/vault.service.ts`:
- Around line 80-101: The current check-then-write using
folderIpnsRepository.findOne followed by save has a TOCTOU race; replace it with
an atomic upsert like the pattern used in recordPin so the insert-or-update
happens in one DB statement: use the repository.create(...) or
repository.createQueryBuilder() and call orUpdate (or the equivalent TypeORM
upsert) to insert the row with isRoot=true or update the existing row to set
isRoot=true on conflict, removing the findOne/save flow (remove
existingFolderIpns and rootFolderIpns branching) and ensure the unique key
(ipnsName+userId) is used as the conflict target.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 7930a12f-51ad-4db0-a54d-9c2243a31a4a
📒 Files selected for processing (1)
apps/api/src/vault/vault.service.ts
There was a problem hiding this comment.
Pull request overview
Fixes a vault initialization failure in the API when the root folder IPNS row is already created earlier in the init flow (via /ipns/publish), preventing a unique constraint violation during POST /vault/init.
Changes:
- Updates
VaultService.initializeVault()to check for an existingfolder_ipnsrow for(userId, rootIpnsName). - If the row exists, flips
isRoottotrueinstead of inserting a duplicate; otherwise inserts a new root tracking row.
Replaces the TOCTOU-prone findOne + conditional save with an optimistic insert that catches the duplicate key constraint (PG 23505) and falls back to an update. Eliminates the race window and removes one DB round-trip in the common case (no pre-existing row). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Entire-Checkpoint: ad1a5f03234a
Tests both the PG 23505 (duplicate key) fallback to update and the rethrow path for non-duplicate errors. Brings vault.service.ts branch coverage from 76.31% to 81.57% (above 77% threshold). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 15d7dce09a4a
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #351 +/- ##
==========================================
+ Coverage 51.21% 51.23% +0.02%
==========================================
Files 115 115
Lines 9256 9260 +4
Branches 747 748 +1
==========================================
+ Hits 4740 4744 +4
Misses 4337 4337
Partials 179 179
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
|
@coderabbitai please review |
🧩 Analysis chain🏁 Script executed: Length of output: 11463
|
Summary
duplicate key value violates unique constraint "UQ_folder_ipns_user_ipns"error onPOST /vault/init/vault/init. The IPNS publish endpoint creates afolder_ipnsrow on first publish. Then/vault/inittried to blindly insert a second row for the same root IPNS name, hitting the unique constraint.folder_ipnsentry already exists (created by the publish step) and updateisRoot=trueinstead of inserting a duplicate.Root Cause
After PR #349, the vault initialization sequence is:
folder_ipnsrow via publish endpoint)folder_ipnsrow)POST /vault/init— tries to insert afolder_ipnsrow for root IPNS name → duplicateTest plan
🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests