import { db, schema } from "@superlog/db"; import { and, eq } from "drizzle-orm"; // Org context resolution backed by Better Auth's session + organization // plugin. Replaces the old Clerk-driven resolveActiveOrgContext in // clerk-orgs.ts: identity now comes from our local `users` + `org_members` // tables, so this is a pure DB read with no upstream sync calls. type SyncedUser = typeof schema.users.$inferSelect; type SyncedOrg = typeof schema.orgs.$inferSelect; type SyncedProject = typeof schema.projects.$inferSelect; type ResolveActiveOrgOptions = { userId: string; preferredOrgId?: string | null; preferredProjectId?: string | null; }; export type ActiveOrgContext = { user: SyncedUser; org: SyncedOrg; project: SyncedProject; }; export type MaybeActiveOrgContext = | ActiveOrgContext | { user: SyncedUser; org: null; project: null }; async function ensureProjectForOrg(orgId: string): Promise { const existing = await db.query.projects.findFirst({ where: eq(schema.projects.orgId, orgId), }); if (existing) return existing; const inserted = await db .insert(schema.projects) .values({ orgId, name: "Default", slug: "default" }) .returning(); const project = inserted[0]; if (!project) throw new Error("failed to create default project"); await db .insert(schema.projectAutomationSettings) .values({ projectId: project.id }) .onConflictDoNothing({ target: schema.projectAutomationSettings.projectId }); return project; } async function resolveProjectForOrg( orgId: string, preferredProjectId: string | null, ): Promise { if (preferredProjectId) { const candidate = await db.query.projects.findFirst({ where: and(eq(schema.projects.id, preferredProjectId), eq(schema.projects.orgId, orgId)), }); if (candidate) return candidate; } return ensureProjectForOrg(orgId); } // Returns the user's active org + project, or `org: null, project: null` when // the user hasn't created their first org yet. Use this on endpoints that // must keep working for pre-org users (currently only /api/me — every other // endpoint requires an org and uses resolveActiveOrgContext). export async function resolveMaybeActiveOrgContext( options: ResolveActiveOrgOptions, ): Promise { const user = await db.query.users.findFirst({ where: eq(schema.users.id, options.userId) }); if (!user) throw new Error(`user ${options.userId} not found`); const preferredProjectId = options.preferredProjectId ?? user.activeProjectId ?? null; if (options.preferredOrgId) { const membership = await db.query.orgMembers.findFirst({ where: and( eq(schema.orgMembers.userId, options.userId), eq(schema.orgMembers.orgId, options.preferredOrgId), ), }); if (membership) { const org = await db.query.orgs.findFirst({ where: eq(schema.orgs.id, membership.orgId) }); if (org) { const project = await resolveProjectForOrg(org.id, preferredProjectId); return { user, org, project }; } } } const firstMembership = await db.query.orgMembers.findFirst({ where: eq(schema.orgMembers.userId, options.userId), }); if (!firstMembership) return { user, org: null, project: null }; const org = await db.query.orgs.findFirst({ where: eq(schema.orgs.id, firstMembership.orgId) }); if (!org) throw new Error("org membership references missing org"); const project = await resolveProjectForOrg(org.id, preferredProjectId); return { user, org, project }; } export async function resolveActiveOrgContext( options: ResolveActiveOrgOptions, ): Promise { const ctx = await resolveMaybeActiveOrgContext(options); if (!ctx.org) throw new Error("no organization access"); return ctx; }