import { and, asc, eq } from "drizzle-orm"; import { termixIdentities, termixIdentityKeys } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; export type TermixIdentityRecord = typeof termixIdentities.$inferSelect; export type NewTermixIdentityRecord = typeof termixIdentities.$inferInsert; export type TermixIdentityUpdate = Partial< Pick >; export type TermixIdentityKeyRecord = typeof termixIdentityKeys.$inferSelect; export type NewTermixIdentityKeyRecord = typeof termixIdentityKeys.$inferInsert; export type TermixIdentityKeyUpdate = Partial< Pick >; export class TermixIdentityRepository { constructor( private readonly context: DatabaseContext, private readonly onWrite?: () => void | Promise, ) {} async findIdentityForUser( userId: string, ): Promise { const rows = await this.context.drizzle .select() .from(termixIdentities) .where(eq(termixIdentities.userId, userId)) .limit(1); return rows[0] ?? null; } async findIdentityByHandle( handle: string, ): Promise { const rows = await this.context.drizzle .select() .from(termixIdentities) .where(eq(termixIdentities.handle, handle)) .limit(1); return rows[0] ?? null; } async isHandleTaken(handle: string): Promise { const rows = await this.context.drizzle .select({ id: termixIdentities.id }) .from(termixIdentities) .where(eq(termixIdentities.handle, handle)) .limit(1); return rows.length > 0; } async createIdentity( identity: NewTermixIdentityRecord, ): Promise { const rows = await this.context.drizzle .insert(termixIdentities) .values(identity) .returning(); await this.afterWrite(); return rows[0]; } async updateIdentityForUser( userId: string, update: TermixIdentityUpdate, ): Promise { const rows = await this.context.drizzle .update(termixIdentities) .set(update) .where(eq(termixIdentities.userId, userId)) .returning(); if (rows.length > 0) { await this.afterWrite(); } return rows[0] ?? null; } async deleteIdentityForUser(userId: string): Promise { const rows = await this.context.drizzle .delete(termixIdentities) .where(eq(termixIdentities.userId, userId)) .returning({ id: termixIdentities.id }); if (rows.length > 0) { await this.afterWrite(); } return rows.length > 0; } async deleteByUserId(userId: string): Promise<{ identitiesDeleted: number; keysDeleted: number; }> { const keyRows = await this.context.drizzle .delete(termixIdentityKeys) .where(eq(termixIdentityKeys.userId, userId)) .returning({ id: termixIdentityKeys.id }); const identityRows = await this.context.drizzle .delete(termixIdentities) .where(eq(termixIdentities.userId, userId)) .returning({ id: termixIdentities.id }); if (keyRows.length > 0 || identityRows.length > 0) { await this.afterWrite(); } return { identitiesDeleted: identityRows.length, keysDeleted: keyRows.length, }; } async listKeysByIdentityId( identityId: number, ): Promise { return this.context.drizzle .select() .from(termixIdentityKeys) .where(eq(termixIdentityKeys.identityId, identityId)) .orderBy(asc(termixIdentityKeys.id)); } async listEnabledKeysByIdentityId( identityId: number, ): Promise { return this.context.drizzle .select() .from(termixIdentityKeys) .where( and( eq(termixIdentityKeys.identityId, identityId), eq(termixIdentityKeys.enabled, true), ), ) .orderBy(asc(termixIdentityKeys.id)); } async listLinkedCredentialIds(identityId: number): Promise { const rows = await this.context.drizzle .select({ credentialId: termixIdentityKeys.credentialId }) .from(termixIdentityKeys) .where( and( eq(termixIdentityKeys.identityId, identityId), eq(termixIdentityKeys.enabled, true), ), ); return Array.from( new Set( rows .map((row) => row.credentialId) .filter( (credentialId): credentialId is number => credentialId !== null, ), ), ); } async createKey( key: NewTermixIdentityKeyRecord, ): Promise { const rows = await this.context.drizzle .insert(termixIdentityKeys) .values(key) .returning(); await this.afterWrite(); return rows[0]; } async updateKeyForUser( userId: string, id: number, update: TermixIdentityKeyUpdate, ): Promise { const rows = await this.context.drizzle .update(termixIdentityKeys) .set(update) .where( and( eq(termixIdentityKeys.id, id), eq(termixIdentityKeys.userId, userId), ), ) .returning(); if (rows.length > 0) { await this.afterWrite(); } return rows[0] ?? null; } async deleteKeyForUser(userId: string, id: number): Promise { const rows = await this.context.drizzle .delete(termixIdentityKeys) .where( and( eq(termixIdentityKeys.id, id), eq(termixIdentityKeys.userId, userId), ), ) .returning({ id: termixIdentityKeys.id }); if (rows.length > 0) { await this.afterWrite(); } return rows.length > 0; } async findKeyForUser( userId: string, id: number, ): Promise { const rows = await this.context.drizzle .select() .from(termixIdentityKeys) .where( and( eq(termixIdentityKeys.id, id), eq(termixIdentityKeys.userId, userId), ), ) .limit(1); return rows[0] ?? null; } private async afterWrite(): Promise { await this.onWrite?.(); } }