Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | 796x 197x 197x 4x 4x 201x 16x 16x 517x 517x 13x 13x 1x 1x 1x 14x 12x 299x | import { eq, and } from "drizzle-orm";
import type { Database } from "@/lib/db";
import { membershipsTable, user } from "@/lib/db/schema";
import type { CollectionMembership, MembershipRole } from "@/types";
import { generateId } from "@/lib/id";
export class MembershipRepository {
constructor(private db: Database) {}
async create(data: {
id?: string;
collectionId: string;
userId: string;
role?: "owner" | "curator";
}): Promise<CollectionMembership> {
const [mem] = await this.db
.insert(membershipsTable)
.values(this.toInsertValues(data))
.returning();
return this.mapToMembership(mem);
}
async createIfAbsent(data: {
id?: string;
collectionId: string;
userId: string;
role?: "owner" | "curator";
}): Promise<CollectionMembership | null> {
// onConflictDoNothing on the (collection_id, user_id) unique index: a
// duplicate insert is a no-op that raises NO error, so this is safe to call
// inside a transaction (postgres-js would otherwise latch a 23505 error and
// roll back the whole tx at scope end). Returns null when nothing was inserted.
const [mem] = await this.db
.insert(membershipsTable)
.values(this.toInsertValues(data))
.onConflictDoNothing({
target: [membershipsTable.collectionId, membershipsTable.userId],
})
.returning();
return mem ? this.mapToMembership(mem) : null;
}
private toInsertValues(data: {
id?: string;
collectionId: string;
userId: string;
role?: "owner" | "curator";
}) {
return {
id: data.id || generateId("mem"),
collectionId: data.collectionId,
userId: data.userId,
role: data.role ?? "curator",
createdAt: new Date(),
updatedAt: new Date(),
};
}
async findById(id: string): Promise<CollectionMembership | null> {
const [mem] = await this.db
.select()
.from(membershipsTable)
.where(eq(membershipsTable.id, id));
return mem ? this.mapToMembership(mem) : null;
}
async findByUser(userId: string, collectionId: string): Promise<CollectionMembership | null> {
const [mem] = await this.db
.select()
.from(membershipsTable)
.where(
and(
eq(membershipsTable.userId, userId),
eq(membershipsTable.collectionId, collectionId),
),
);
return mem ? this.mapToMembership(mem) : null;
}
async findByCollection(collectionId: string): Promise<CollectionMembership[]> {
// LEFT JOIN the user table so each accepted member carries the person-side
// name + email the Members screen renders (avatar initial + email line).
// The base memberships table holds only user_id; without this join those
// optional fields stay undefined. LEFT (not INNER) so a membership whose
// user row somehow went missing still lists, just without name/email.
const rows = await this.db
.select({
membership: membershipsTable,
name: user.name,
email: user.email,
})
.from(membershipsTable)
.leftJoin(user, eq(membershipsTable.userId, user.id))
.where(eq(membershipsTable.collectionId, collectionId));
return rows.map((r) => ({
...this.mapToMembership(r.membership),
name: r.name ?? undefined,
email: r.email ?? undefined,
}));
}
async update(id: string, _data: Partial<CollectionMembership>): Promise<CollectionMembership> {
// Allow-list: no caller-supplied fields are permitted through membership update.
// Role, flags, collectionId, and userId are immutable after creation.
// Only updatedAt is written to mark the row touched.
const [mem] = await this.db
.update(membershipsTable)
.set({ updatedAt: new Date() })
.where(eq(membershipsTable.id, id))
.returning();
return this.mapToMembership(mem);
}
async delete(id: string): Promise<void> {
await this.db.delete(membershipsTable).where(eq(membershipsTable.id, id));
}
async deleteByCollection(collectionId: string): Promise<void> {
await this.db.delete(membershipsTable).where(eq(membershipsTable.collectionId, collectionId));
}
// Delete every membership for the given user across all collections.
// Used during account deletion to remove the user's curator memberships in
// borrowed collections (after child rows have been removed). Account-deletion only.
async deleteByUser(userId: string): Promise<void> {
await this.db.delete(membershipsTable).where(eq(membershipsTable.userId, userId));
}
private mapToMembership(row: typeof membershipsTable.$inferSelect): CollectionMembership {
return {
id: row.id,
collectionId: row.collectionId,
userId: row.userId,
role: row.role as MembershipRole,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
};
}
}
|