Skip to content

fix(organizations): membership user_org_unique partial index root-cause#3856

Merged
PierreBrisorgueil merged 3 commits into
masterfrom
fix/3841-membership-unique-index
Jun 13, 2026
Merged

fix(organizations): membership user_org_unique partial index root-cause#3856
PierreBrisorgueil merged 3 commits into
masterfrom
fix/3841-membership-unique-index

Conversation

@PierreBrisorgueil

@PierreBrisorgueil PierreBrisorgueil commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Root-cause fix for the membership (userId, organizationId) partial-unique index that never materialized on any deployed DB.

  • Root cause: the schema declared partialFilterExpression: { userId: { $exists: true, $ne: null } }. MongoDB does not support $ne in a partialFilterExpression, so the index build failed silently — mongoose autoIndex emits the error on the unobserved 'index' event, so the duplicate-membership guard had no DB backstop anywhere.
  • Fix (schema, not just a migration): the schema twin now declares partialFilterExpression: { userId: { $type: 'objectId' } } (null userId rows are type null → still excluded, preserving the original intent) with the explicit name user_org_unique so autoIndex/syncIndexes stay idempotent.
  • Migration: 20260612120000-membership-user-org-unique-index.js follows the house check-then-create pattern (pre-check duplicates → abort with a PII-safe ids-only sample; tolerate NamespaceNotFound on a fresh DB; exact-shape hasIndex check; create; idempotent; no-op down()).

Test plan

  • organizations.membershipUserOrgUniqueIndex.migration.integration.tests.js — index exists with the exact spec after up(), idempotent re-run, duplicate pre-check abort, and an E11000 DB-backstop (two pending rows same (userId, organizationId) → second insert rejects). 7/7.
  • Full organizations regression: 166 tests green.

Guardrails check

  • npm run lint clean
  • Public-OSS clean (no downstream names/domains/infra/board-ids)
  • Migration is idempotent; abort-on-bad-data blocks a deploy with colliding rows by design

Closes #3841

Summary by CodeRabbit

Release Notes

Chores

  • Strengthened data integrity by adding database-level enforcement for unique user-organization memberships.

MongoDB does not support $ne in partialFilterExpression, so the
schema-declared { userId: { $exists: true, $ne: null } } partial-unique
index always failed server-side; mongoose autoIndex emits that failure
on the model's unlistened 'index' event, so the duplicate-membership
unique guard never materialized on any deployed database. Replace the
spec with { userId: { $type: 'objectId' } } (same intent: null-userId
rows stay excluded), name it user_org_unique, and add the authoritative
migration (duplicate pre-check abort, divergent same-key index drop,
idempotent re-run) with the schema declaring the identical twin.
refs #3841
Prove the user_org_unique index is live at the model level: a second
pending row for the same (userId, organizationId) pair rejects with
E11000 behind the racy findOne-then-create guards in addMember and
createJoinRequest, null-userId rows stay outside the partial index, and
syncIndexes() confirms the schema twin matches the migration spec.
refs #3841
Copilot AI review requested due to automatic review settings June 13, 2026 12:26
@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

More reviews will be available in 54 minutes and 36 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

Run ID: adcd81cf-efb2-4dd6-9f5a-fd471fef44b9

📥 Commits

Reviewing files that changed from the base of the PR and between b316a51 and ddd4e75.

📒 Files selected for processing (1)
  • modules/organizations/tests/organizations.membershipUserOrgUniqueIndex.migration.integration.tests.js

Walkthrough

A new MongoDB migration enforces a partial unique index on (userId, organizationId) for memberships with pre-flight duplicate detection and idempotent index management. The Mongoose model schema is updated to declare the same index with explicit naming and BSON type filtering. Integration tests validate migration behavior across idempotency, schema alignment, duplicate rejection, and DB-level uniqueness.

Changes

Membership unique index enforcement

Layer / File(s) Summary
Migration up/down functions
modules/organizations/migrations/20260612120000-membership-user-org-unique-index.js
up() aggregates to detect duplicate (userId, organizationId) pairs for objectId userId rows before modifying indexes; snapshots indexes with missing-collection error handling; compares key shapes, order, and options against the canonical spec; drops divergent indexes by shape or name mismatch; and creates the user_org_unique index idempotently. down() logs a warning and performs no rollback.
Model schema alignment
modules/organizations/models/organizations.membership.model.mongoose.js
Membership compound unique index updated to filter by BSON type { userId: { $type: 'objectId' } } instead of existence checks, with explicit index name user_org_unique. Documentation expanded to describe the partial filter change and authoritative migration relationship.
Integration test suite
modules/organizations/tests/organizations.membershipUserOrgUniqueIndex.migration.integration.tests.js
Seven test scenarios verify up() creates the canonical index, remains idempotent on re-run, replaces misnamed same-key indexes, rejects migration if duplicates exist before index work, aligns with model via syncIndexes(), enforces DB-level uniqueness at error code 11000, and excludes userId: null rows from the partial constraint. Includes lifecycle hooks and collection cleanup.

🎯 3 (Moderate) | ⏱️ ~25 minutes

Fix

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(organizations): membership user_org_unique partial index root-cause' clearly and specifically identifies the primary change: fixing the root cause of a missing partial-unique index on memberships.
Description check ✅ Passed The description comprehensively covers root cause, fix details (schema change), migration approach, test plan, and guardrails checks, though it does not follow the exact template structure with all sections (e.g., 'Scope' and 'Optional: Infra/Stack alignment details' sections are missing).
Linked Issues check ✅ Passed All coding requirements from #3841 are met: the invalid partialFilterExpression is replaced with a valid BSON $type filter, an idempotent migration with duplicate pre-check is provided, indexes are verified through integration tests, and DB backstop enforcement is confirmed via E11000 duplicate error testing.
Out of Scope Changes check ✅ Passed All changes are in-scope and directly address the linked issue #3841: schema fix, idempotent migration, and integration tests for the partial-unique index—no unrelated changes detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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 fix/3841-membership-unique-index

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.

❤️ Share

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

@codecov

codecov Bot commented Jun 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.99%. Comparing base (755af6e) to head (ddd4e75).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3856      +/-   ##
==========================================
+ Coverage   91.96%   91.99%   +0.03%     
==========================================
  Files         160      160              
  Lines        5289     5310      +21     
  Branches     1698     1708      +10     
==========================================
+ Hits         4864     4885      +21     
  Misses        337      337              
  Partials       88       88              
Flag Coverage Δ
integration 59.92% <ø> (+0.08%) ⬆️
unit 72.54% <ø> (-0.14%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 755af6e...ddd4e75. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI 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.

Pull request overview

Root-cause fix to ensure the intended (userId, organizationId) partial-unique membership constraint actually exists at the database level, restoring a race-proof backstop for duplicate membership creation.

Changes:

  • Updates the Membership schema’s compound unique index to use a MongoDB-supported partialFilterExpression ($type: 'objectId') and an explicit stable index name (user_org_unique).
  • Adds an idempotent migration that pre-checks for duplicates, drops divergent/conflicting indexes, and creates the canonical partial-unique index.
  • Adds an integration test suite validating the migration behavior, idempotency, schema/migration alignment (syncIndexes()), and real E11000 enforcement.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
modules/organizations/models/organizations.membership.model.mongoose.js Fixes the schema index spec to a MongoDB-supported partial filter and pins the canonical index name for idempotent sync.
modules/organizations/migrations/20260612120000-membership-user-org-unique-index.js Adds an idempotent, safety-checked migration to ensure the canonical partial-unique index exists and is correctly shaped.
modules/organizations/tests/organizations.membershipUserOrgUniqueIndex.migration.integration.tests.js Adds integration coverage for the migration + DB backstop behavior (exact spec, idempotency, abort-on-duplicates, E11000).

@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: 1

🤖 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
`@modules/organizations/tests/organizations.membershipUserOrgUniqueIndex.migration.integration.tests.js`:
- Around line 50-53: Add a JSDoc header above the named helper function
findIndex describing its parameter and return value: document the parameter
"name" as a string with `@param` {string} name - index name to find, and document
the return as a Promise resolving to the index object or undefined/null with
`@returns` {Promise<Object|undefined>} (or the actual index type used in the
tests). Keep the description concise and place the JSDoc immediately above the
findIndex function declaration.
🪄 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

Run ID: 74c0f53c-d41f-4b06-8eed-03bf02ff58ed

📥 Commits

Reviewing files that changed from the base of the PR and between 137f37e and b316a51.

📒 Files selected for processing (3)
  • modules/organizations/migrations/20260612120000-membership-user-org-unique-index.js
  • modules/organizations/models/organizations.membership.model.mongoose.js
  • modules/organizations/tests/organizations.membershipUserOrgUniqueIndex.migration.integration.tests.js

per coding guidelines (every function requires @param + @returns).
refs #3841
@PierreBrisorgueil PierreBrisorgueil merged commit 01740e6 into master Jun 13, 2026
8 checks passed
@PierreBrisorgueil PierreBrisorgueil deleted the fix/3841-membership-unique-index branch June 13, 2026 13:15
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.

🐛 membership partial-unique index (userId,organizationId) declared in the model but absent on deployed DBs

2 participants