| 94 | }; |
| 95 | |
| 96 | export const makeUserStore = (db: DrizzleDb) => { |
| 97 | const getOrganization = (id: string) => readOrganization(db, id); |
| 98 | |
| 99 | // Existing rows keep their slug (stable across renames, so org URLs |
| 100 | // survive) and only refresh their name — and only from a payload at least |
| 101 | // as new as the one that last named it (`organizationAcceptsName`): a |
| 102 | // sign-in whose membership list was fetched before a rename would |
| 103 | // otherwise revert the rename after it landed. A row marked deleted is |
| 104 | // returned as it is: the organization is gone, and nothing a feeder still |
| 105 | // holds about it (a name, a membership fetched before the deletion) is |
| 106 | // written — never re-minted live, never renamed. A row the mirror does not |
| 107 | // hold is minted live (`insertOrganization`). |
| 108 | const upsertOrganization = async (org: OrganizationPayload) => { |
| 109 | const existing = await getOrganization(org.id); |
| 110 | if (existing) { |
| 111 | if (existing.deletedAt !== null) return existing; |
| 112 | const [updated] = await db |
| 113 | .update(organizations) |
| 114 | .set({ name: org.name, workosUpdatedAt: org.updatedAt }) |
| 115 | .where(and(eq(organizations.id, org.id), organizationAcceptsName(org.updatedAt))) |
| 116 | .returning(); |
| 117 | return updated ?? existing; |
| 118 | } |
| 119 | return insertOrganization(db, { |
| 120 | id: org.id, |
| 121 | name: org.name, |
| 122 | workosUpdatedAt: org.updatedAt, |
| 123 | deletedAt: null, |
| 124 | }); |
| 125 | }; |
| 126 | |
| 127 | return { |
| 128 | // --- Accounts --- |
| 129 | |
| 130 | ensureAccount: async (id: string) => { |
| 131 | const [result] = await db.insert(accounts).values({ id }).onConflictDoNothing().returning(); |
| 132 | return result ?? (await db.select().from(accounts).where(eq(accounts.id, id)))[0]!; |
| 133 | }, |
| 134 | |
| 135 | getAccount: async (id: string) => { |
| 136 | const rows = await db.select().from(accounts).where(eq(accounts.id, id)); |
| 137 | return rows[0] ?? null; |
| 138 | }, |
| 139 | |
| 140 | // --- Organizations --- |
| 141 | |
| 142 | upsertOrganization, |
| 143 | |
| 144 | getOrganization, |
| 145 | |
| 146 | getOrganizationBySlug: async (slug: string) => { |
| 147 | const rows = await db.select().from(organizations).where(eq(organizations.slug, slug)); |
| 148 | return rows[0] ?? null; |
| 149 | }, |
| 150 | |
| 151 | // Mark an org deleted, refusing every membership authorization against |
| 152 | // it from this moment. The FIRST step of cloud's deletion flow, taken |
| 153 | // before the WorkOS delete and the local purge, so a failure in either |