import { and, asc, eq } from "drizzle-orm"; import { termixIdentities, termixIdentityKeys } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; import { rowsAffected } from "./mutation-result.js"; import { insertReturning, updateReturning } from "./returning.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 insertReturning( this.context, termixIdentities, identity, ); await this.afterWrite(); return rows[0]; } async updateIdentityForUser( userId: string, update: TermixIdentityUpdate, ): Promise { const rows = await updateReturning( this.context, termixIdentities, update, eq(termixIdentities.userId, userId), ); if (rows.length > 0) { await this.afterWrite(); } return rows[0] ?? null; } async deleteIdentityForUser(userId: string): Promise { const result = await this.context.drizzle .delete(termixIdentities) .where(eq(termixIdentities.userId, userId)); if (rowsAffected(result) > 0) { await this.afterWrite(); } return rowsAffected(result) > 0; } async deleteByUserId(userId: string): Promise<{ identitiesDeleted: number; keysDeleted: number; }> { const keyResult = await this.context.drizzle .delete(termixIdentityKeys) .where(eq(termixIdentityKeys.userId, userId)); const result = await this.context.drizzle .delete(termixIdentities) .where(eq(termixIdentities.userId, userId)); if (rowsAffected(keyResult) > 0 || rowsAffected(result) > 0) { await this.afterWrite(); } return { identitiesDeleted: rowsAffected(result), keysDeleted: rowsAffected(keyResult), }; } 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 insertReturning(this.context, termixIdentityKeys, key); await this.afterWrite(); return rows[0]; } async updateKeyForUser( userId: string, id: number, update: TermixIdentityKeyUpdate, ): Promise { const rows = await updateReturning( this.context, termixIdentityKeys, update, and(eq(termixIdentityKeys.id, id), eq(termixIdentityKeys.userId, userId)), ); if (rows.length > 0) { await this.afterWrite(); } return rows[0] ?? null; } async deleteKeyForUser(userId: string, id: number): Promise { const result = await this.context.drizzle .delete(termixIdentityKeys) .where( and( eq(termixIdentityKeys.id, id), eq(termixIdentityKeys.userId, userId), ), ); if (rowsAffected(result) > 0) { await this.afterWrite(); } return rowsAffected(result) > 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?.(); } }