(db: DrizzleDb)
| 598 | // or the one the store opens per `upsertMembership`. Each answers whether |
| 599 | // it wrote a row; the public shape and the transactions translate that. |
| 600 | const makeWrites = (db: DrizzleDb) => { |
| 601 | const ensureAccount = (id: string) => |
| 602 | db.insert(accounts).values({ id }).onConflictDoNothing({ target: accounts.id }); |
| 603 | |
| 604 | // The membership upsert under the row guard (`membershipAcceptsPayload`) |
| 605 | // and the account tombstone, never the organization mark. |
| 606 | const writeMembership = async (membership: WorkOsMirrorMembership): Promise<boolean> => { |
| 607 | await ensureAccount(membership.accountId); |
| 608 | // Never for a DELETED user. The row guard below orders a payload against |
| 609 | // the membership row it would overwrite; a membership the mirror has not |
| 610 | // seen yet has no row, so the guard cannot refuse the INSERT — and a |
| 611 | // feeder that fetched the membership before the user was deleted and |
| 612 | // writes it after (a stalled login list, the backfill's older listing) |
| 613 | // would insert it live. The account tombstone is the one row a deleted |
| 614 | // user always leaves behind, so it is consulted first, by identity: |
| 615 | // WorkOS never reuses a user id, so no payload naming a tombstoned |
| 616 | // account is ever current. |
| 617 | // |
| 618 | // Read FOR SHARE, held until the transaction `db` belongs to commits: a |
| 619 | // `deleteUser` applying the deletion at this moment holds the row FOR NO |
| 620 | // KEY UPDATE until ITS commit, so this read waits for it and sees the |
| 621 | // tombstone — and a deletion that arrives after this read waits for this |
| 622 | // transaction, then tombstones the membership it inserted. Unlocked, a |
| 623 | // deletion could land between this read and the insert below and leave |
| 624 | // a live membership for a deleted user. |
| 625 | const locked = await db |
| 626 | .select({ email: accounts.email, workosUpdatedAt: accounts.workosUpdatedAt }) |
| 627 | .from(accounts) |
| 628 | .where(eq(accounts.id, membership.accountId)) |
| 629 | .for("share"); |
| 630 | const account = locked[0]; |
| 631 | // `ensureAccount` guarantees the row; accounts are tombstoned, never |
| 632 | // deleted, so a missing one is refused like a tombstone, not written for. |
| 633 | if (account === undefined || isAccountTombstone(account)) return false; |
| 634 | // Never under a DELETED membership id. The row guard below can only |
| 635 | // refuse against the id the row holds; a delete of THIS id that found |
| 636 | // the row under another id (see the header) left only the ledger entry |
| 637 | // behind, and that is what refuses the payload here. |
| 638 | if (await membershipIdDeleted(db, membership.id)) return false; |
| 639 | const written = await db |
| 640 | .insert(memberships) |
| 641 | .values({ |
| 642 | accountId: membership.accountId, |
| 643 | organizationId: membership.organizationId, |
| 644 | membershipId: membership.id, |
| 645 | role: membership.role, |
| 646 | status: membership.status, |
| 647 | workosUpdatedAt: membership.updatedAt, |
| 648 | }) |
| 649 | .onConflictDoUpdate({ |
| 650 | target: [memberships.accountId, memberships.organizationId], |
| 651 | set: { |
| 652 | membershipId: membership.id, |
| 653 | role: membership.role, |
| 654 | status: membership.status, |
| 655 | workosUpdatedAt: membership.updatedAt, |
| 656 | // A replacement taking over a tombstone is live again; a row that |
| 657 | // was not tombstoned had nothing here. |
no test coverage detected