| 19 | export type Organization = typeof organizations.$inferSelect; |
| 20 | |
| 21 | export const makeUserStore = (db: DrizzleDb) => { |
| 22 | const getOrganization = async (id: string) => { |
| 23 | const rows = await db.select().from(organizations).where(eq(organizations.id, id)); |
| 24 | return rows[0] ?? null; |
| 25 | }; |
| 26 | |
| 27 | const slugTaken = async (slug: string) => { |
| 28 | const rows = await db |
| 29 | .select({ id: organizations.id }) |
| 30 | .from(organizations) |
| 31 | .where(eq(organizations.slug, slug)); |
| 32 | return rows.length > 0; |
| 33 | }; |
| 34 | |
| 35 | // Insert a brand-new org row carrying a freshly-minted slug. `ON CONFLICT DO |
| 36 | // NOTHING` (no target) absorbs BOTH unique violations without throwing: an |
| 37 | // id collision (the org was mirrored concurrently) and a slug collision (the |
| 38 | // candidate was claimed by a different org). Returns the inserted row, or |
| 39 | // null when either conflict swallowed the insert — the caller decides whether |
| 40 | // to re-read (id race) or retry with a new candidate (slug race). |
| 41 | const tryInsertOrg = async (id: string, name: string, slug: string) => { |
| 42 | const [row] = await db |
| 43 | .insert(organizations) |
| 44 | .values({ id, name, slug }) |
| 45 | .onConflictDoNothing() |
| 46 | .returning(); |
| 47 | return row ?? null; |
| 48 | }; |
| 49 | |
| 50 | // Every new org row is born with a slug — there is no nullable window and no |
| 51 | // self-healing. Existing rows keep their slug (stable across renames, so org |
| 52 | // URLs survive) and only refresh their name. |
| 53 | const upsertOrganization = async (org: { id: string; name: string }) => { |
| 54 | const existing = await getOrganization(org.id); |
| 55 | if (existing) { |
| 56 | const [updated] = await db |
| 57 | .update(organizations) |
| 58 | .set({ name: org.name }) |
| 59 | .where(eq(organizations.id, org.id)) |
| 60 | .returning(); |
| 61 | return updated ?? existing; |
| 62 | } |
| 63 | for (let attempt = 0; attempt < 4; attempt++) { |
| 64 | const slug = await generateOrgSlug(org.name, slugTaken); |
| 65 | const inserted = await tryInsertOrg(org.id, org.name, slug); |
| 66 | if (inserted) return inserted; |
| 67 | // The insert was swallowed by a conflict. If the id now exists, a |
| 68 | // concurrent request mirrored it — return that row. Otherwise the slug |
| 69 | // candidate collided; loop and mint a fresh one. |
| 70 | const fresh = await getOrganization(org.id); |
| 71 | if (fresh) return fresh; |
| 72 | } |
| 73 | // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: slug minting exhausted retries; surfacing loudly beats a silently unslugged org |
| 74 | throw new Error(`unable to mint a slug for organization ${org.id}`); |
| 75 | }; |
| 76 | |
| 77 | return { |
| 78 | // --- Accounts --- |