Collapse the two duplicate pairs onto the correct-NUSNET account#51
Closed
patrick-steve wants to merge 1 commit into
Closed
Collapse the two duplicate pairs onto the correct-NUSNET account#51patrick-steve wants to merge 1 commit into
patrick-steve wants to merge 1 commit into
Conversation
Keri-ann and Danvern each have two User rows that share User.userID but not email. Every role, booking and gate key is canonicalUserID(email), so those pairs are two identities under one label -- and the bookings already sit on the correct-NUSNET side, so "keep the one with the right NUSNET id" is also the choice that moves no data. The script asserts the doomed canonical owns nothing and that its UserRole is baseline before deleting anything; the full name is lifted onto the survivor explicitly rather than by heuristic. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ebVcP5QpHkhDkXfu9gnqt
|
Deployment failed with the following error: View Documentation: https://vercel.com/docs/accounts/team-members-and-roles |
There was a problem hiding this comment.
Pull request overview
Adds a targeted remediation script to resolve two “duplicate identity” user pairs that merge-by-canonical could not automatically collapse, ensuring each pair is collapsed onto the account whose email-derived canonicalUserID matches the correct NUSNET id, while asserting the doomed canonical owns no dependent data before deletion.
Changes:
- Introduce
fix-nusnet-survivor.mjsto (dry-run by default) validate two hard-coded user pairs, verify the doomed canonical has zero dependent rows, and (on--commit) update survivordisplayName+ delete the duplicateUserrow. - Add safety checks around dependent collections and baseline
UserRolebefore allowing deletion. - Persist a JSON “before-state” backup for auditability.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+84
to
+96
| const keepCanon = canonicalUserID(pair.keepEmail); | ||
| const dropCanon = canonicalUserID(pair.dropEmail); | ||
|
|
||
| if (keepCanon !== pair.userID) { | ||
| return abort( | ||
| `${pair.keepEmail} canonicalises to ${keepCanon}, not ${pair.userID}`, | ||
| ); | ||
| } | ||
| if (dropCanon === pair.userID) { | ||
| return abort( | ||
| `${pair.dropEmail} canonicalises to ${pair.userID} — it is not the odd one out`, | ||
| ); | ||
| } |
Comment on lines
+62
to
+77
| /** Collections keyed on the canonical id. The doomed canonical must own zero | ||
| * rows in every one of these, or deleting its account orphans real data. */ | ||
| const REQUIRE_EMPTY = [ | ||
| ["Bookings", (c) => db.bookings.count({ where: { userID: c } })], | ||
| ["BookingLogs", (c) => db.bookingLogs.count({ where: { userID: c } })], | ||
| ["Posts", (c) => db.posts.count({ where: { userID: c } })], | ||
| ["Order", (c) => db.order.count({ where: { userID: c } })], | ||
| ["UserCCA", (c) => db.userCCA.count({ where: { userID: c } })], | ||
| ["Gym", (c) => db.gym.count({ where: { userID: c } })], | ||
| ["CcaHead", (c) => db.ccaHead.count({ where: { userID: c } })], | ||
| ["UserMatric", (c) => db.userMatric.count({ where: { userID: c } })], | ||
| [ | ||
| "ProfileCompletion", | ||
| (c) => db.profileCompletion.count({ where: { userID: c } }), | ||
| ], | ||
| ]; |
Comment on lines
+98
to
+103
| const rows = await db.user.findMany({ | ||
| where: { email: { in: [pair.keepEmail, pair.dropEmail] } }, | ||
| }); | ||
| const survivor = rows.find((r) => r.email === pair.keepEmail); | ||
| const loser = rows.find((r) => r.email === pair.dropEmail); | ||
| if (!survivor || !loser) |
Comment on lines
+166
to
+176
| for (const { pair, survivor, loser, dropCanon, staleRole } of plan) { | ||
| await db.user.update({ | ||
| where: { id: survivor.id }, | ||
| data: { displayName: pair.name }, | ||
| }); | ||
| await db.user.delete({ where: { id: loser.id } }); | ||
| if (staleRole) await db.userRole.delete({ where: { userID: dropCanon } }); | ||
| console.log( | ||
| `${pair.userID}: survivor updated, ${loser.email} deleted, role key ${dropCanon} cleared`, | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
merge-by-canonicalleft two pairs untouched because both members already share aUser.userID. That field is not the identity anything is keyed on — roles, bookings and gates all key oncanonicalUserID(email)(I-1) — so each pair is two identities wearing one label:e1523366@u.nus.edu→E1523366(5 bookings)e153366@u.nus.edu→E153366(0 rows)e1525917@u.nus.edu→E1525917(2 bookings)danvern@u.nus.edu→DANVERN(0 rows)Per the owner: keep the account with the correct NUSNET id. That is also where the dependent data already lives, so no Bookings move. The full display name ("Keri-ann Shi", "Danvern Poo Han Yew") is lifted onto the survivor, which currently carries only a nickname.
Before deleting anything the script asserts the doomed canonical owns zero rows across Bookings/BookingLogs/Posts/Order/UserCCA/Gym/CcaHead/UserMatric/ProfileCompletion, and that its
UserRoleis baseline["resident"].Dry run is clean and
verify-identity-parity.mjspasses. Not yet run with--commit— that deletes production rows and is the owner's call.🤖 Generated with Claude Code
https://claude.ai/code/session_016ebVcP5QpHkhDkXfu9gnqt