2026-07-19 12:29:52 -05:00
|
|
|
import { and, asc, eq } from "drizzle-orm";
|
|
|
|
|
import { termixIdentities, termixIdentityKeys } from "../db/schema.js";
|
|
|
|
|
import type { DatabaseContext } from "./database-context.js";
|
2026-08-06 14:41:39 -05:00
|
|
|
import { rowsAffected } from "./mutation-result.js";
|
|
|
|
|
import { insertReturning, updateReturning } from "./returning.js";
|
2026-07-19 12:29:52 -05:00
|
|
|
|
|
|
|
|
export type TermixIdentityRecord = typeof termixIdentities.$inferSelect;
|
|
|
|
|
export type NewTermixIdentityRecord = typeof termixIdentities.$inferInsert;
|
|
|
|
|
export type TermixIdentityUpdate = Partial<
|
|
|
|
|
Pick<NewTermixIdentityRecord, "handle" | "description" | "updatedAt">
|
|
|
|
|
>;
|
|
|
|
|
|
|
|
|
|
export type TermixIdentityKeyRecord = typeof termixIdentityKeys.$inferSelect;
|
|
|
|
|
export type NewTermixIdentityKeyRecord = typeof termixIdentityKeys.$inferInsert;
|
|
|
|
|
export type TermixIdentityKeyUpdate = Partial<
|
|
|
|
|
Pick<NewTermixIdentityKeyRecord, "enabled" | "label">
|
|
|
|
|
>;
|
|
|
|
|
|
|
|
|
|
export class TermixIdentityRepository {
|
|
|
|
|
constructor(
|
|
|
|
|
private readonly context: DatabaseContext,
|
|
|
|
|
private readonly onWrite?: () => void | Promise<void>,
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
async findIdentityForUser(
|
|
|
|
|
userId: string,
|
|
|
|
|
): Promise<TermixIdentityRecord | null> {
|
|
|
|
|
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<TermixIdentityRecord | null> {
|
|
|
|
|
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<boolean> {
|
|
|
|
|
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<TermixIdentityRecord> {
|
2026-08-06 14:41:39 -05:00
|
|
|
const rows = await insertReturning(
|
|
|
|
|
this.context,
|
|
|
|
|
termixIdentities,
|
|
|
|
|
identity,
|
|
|
|
|
);
|
2026-07-19 12:29:52 -05:00
|
|
|
|
|
|
|
|
await this.afterWrite();
|
|
|
|
|
return rows[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateIdentityForUser(
|
|
|
|
|
userId: string,
|
|
|
|
|
update: TermixIdentityUpdate,
|
|
|
|
|
): Promise<TermixIdentityRecord | null> {
|
2026-08-06 14:41:39 -05:00
|
|
|
const rows = await updateReturning(
|
|
|
|
|
this.context,
|
|
|
|
|
termixIdentities,
|
|
|
|
|
update,
|
|
|
|
|
eq(termixIdentities.userId, userId),
|
|
|
|
|
);
|
2026-07-19 12:29:52 -05:00
|
|
|
|
|
|
|
|
if (rows.length > 0) {
|
|
|
|
|
await this.afterWrite();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return rows[0] ?? null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async deleteIdentityForUser(userId: string): Promise<boolean> {
|
2026-08-06 14:41:39 -05:00
|
|
|
const result = await this.context.drizzle
|
2026-07-19 12:29:52 -05:00
|
|
|
.delete(termixIdentities)
|
2026-08-06 14:41:39 -05:00
|
|
|
.where(eq(termixIdentities.userId, userId));
|
2026-07-19 12:29:52 -05:00
|
|
|
|
2026-08-06 14:41:39 -05:00
|
|
|
if (rowsAffected(result) > 0) {
|
2026-07-19 12:29:52 -05:00
|
|
|
await this.afterWrite();
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-06 14:41:39 -05:00
|
|
|
return rowsAffected(result) > 0;
|
2026-07-19 12:29:52 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async deleteByUserId(userId: string): Promise<{
|
|
|
|
|
identitiesDeleted: number;
|
|
|
|
|
keysDeleted: number;
|
|
|
|
|
}> {
|
2026-08-06 14:41:39 -05:00
|
|
|
const keyResult = await this.context.drizzle
|
2026-07-19 12:29:52 -05:00
|
|
|
.delete(termixIdentityKeys)
|
2026-08-06 14:41:39 -05:00
|
|
|
.where(eq(termixIdentityKeys.userId, userId));
|
2026-07-19 12:29:52 -05:00
|
|
|
|
2026-08-06 14:41:39 -05:00
|
|
|
const result = await this.context.drizzle
|
2026-07-19 12:29:52 -05:00
|
|
|
.delete(termixIdentities)
|
2026-08-06 14:41:39 -05:00
|
|
|
.where(eq(termixIdentities.userId, userId));
|
2026-07-19 12:29:52 -05:00
|
|
|
|
2026-08-06 14:41:39 -05:00
|
|
|
if (rowsAffected(keyResult) > 0 || rowsAffected(result) > 0) {
|
2026-07-19 12:29:52 -05:00
|
|
|
await this.afterWrite();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
2026-08-06 14:41:39 -05:00
|
|
|
identitiesDeleted: rowsAffected(result),
|
|
|
|
|
keysDeleted: rowsAffected(keyResult),
|
2026-07-19 12:29:52 -05:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async listKeysByIdentityId(
|
|
|
|
|
identityId: number,
|
|
|
|
|
): Promise<TermixIdentityKeyRecord[]> {
|
|
|
|
|
return this.context.drizzle
|
|
|
|
|
.select()
|
|
|
|
|
.from(termixIdentityKeys)
|
|
|
|
|
.where(eq(termixIdentityKeys.identityId, identityId))
|
|
|
|
|
.orderBy(asc(termixIdentityKeys.id));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async listEnabledKeysByIdentityId(
|
|
|
|
|
identityId: number,
|
|
|
|
|
): Promise<TermixIdentityKeyRecord[]> {
|
|
|
|
|
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<number[]> {
|
|
|
|
|
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<TermixIdentityKeyRecord> {
|
2026-08-06 14:41:39 -05:00
|
|
|
const rows = await insertReturning(this.context, termixIdentityKeys, key);
|
2026-07-19 12:29:52 -05:00
|
|
|
|
|
|
|
|
await this.afterWrite();
|
|
|
|
|
return rows[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateKeyForUser(
|
|
|
|
|
userId: string,
|
|
|
|
|
id: number,
|
|
|
|
|
update: TermixIdentityKeyUpdate,
|
|
|
|
|
): Promise<TermixIdentityKeyRecord | null> {
|
2026-08-06 14:41:39 -05:00
|
|
|
const rows = await updateReturning(
|
|
|
|
|
this.context,
|
|
|
|
|
termixIdentityKeys,
|
|
|
|
|
update,
|
|
|
|
|
and(eq(termixIdentityKeys.id, id), eq(termixIdentityKeys.userId, userId)),
|
|
|
|
|
);
|
2026-07-19 12:29:52 -05:00
|
|
|
|
|
|
|
|
if (rows.length > 0) {
|
|
|
|
|
await this.afterWrite();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return rows[0] ?? null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async deleteKeyForUser(userId: string, id: number): Promise<boolean> {
|
2026-08-06 14:41:39 -05:00
|
|
|
const result = await this.context.drizzle
|
2026-07-19 12:29:52 -05:00
|
|
|
.delete(termixIdentityKeys)
|
|
|
|
|
.where(
|
|
|
|
|
and(
|
|
|
|
|
eq(termixIdentityKeys.id, id),
|
|
|
|
|
eq(termixIdentityKeys.userId, userId),
|
|
|
|
|
),
|
2026-08-06 14:41:39 -05:00
|
|
|
);
|
2026-07-19 12:29:52 -05:00
|
|
|
|
2026-08-06 14:41:39 -05:00
|
|
|
if (rowsAffected(result) > 0) {
|
2026-07-19 12:29:52 -05:00
|
|
|
await this.afterWrite();
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-06 14:41:39 -05:00
|
|
|
return rowsAffected(result) > 0;
|
2026-07-19 12:29:52 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async findKeyForUser(
|
|
|
|
|
userId: string,
|
|
|
|
|
id: number,
|
|
|
|
|
): Promise<TermixIdentityKeyRecord | null> {
|
|
|
|
|
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<void> {
|
|
|
|
|
await this.onWrite?.();
|
|
|
|
|
}
|
|
|
|
|
}
|