diff --git a/packages/server/src/utils/inviteService.test.ts b/packages/server/src/utils/inviteService.test.ts index 63713514..56f39865 100644 --- a/packages/server/src/utils/inviteService.test.ts +++ b/packages/server/src/utils/inviteService.test.ts @@ -135,23 +135,23 @@ describe('createInvite', () => { it('rejects empty name', () => { const adminId = seedAdmin(); - expect(() => createInvite({ name: '', maxUses: null, expiresAt: null }, adminId)).toThrow(); + expect(() => createInvite({ name: '', maxUses: null, expiresAt: null }, adminId)).toThrow(InviteValidationError); }); it('rejects name longer than 64 chars', () => { const adminId = seedAdmin(); - expect(() => createInvite({ name: 'x'.repeat(65), maxUses: null, expiresAt: null }, adminId)).toThrow(); + expect(() => createInvite({ name: 'x'.repeat(65), maxUses: null, expiresAt: null }, adminId)).toThrow(InviteValidationError); }); it('rejects non-positive maxUses', () => { const adminId = seedAdmin(); - expect(() => createInvite({ name: 'a', maxUses: 0, expiresAt: null }, adminId)).toThrow(); - expect(() => createInvite({ name: 'a', maxUses: -1, expiresAt: null }, adminId)).toThrow(); + expect(() => createInvite({ name: 'a', maxUses: 0, expiresAt: null }, adminId)).toThrow(InviteValidationError); + expect(() => createInvite({ name: 'a', maxUses: -1, expiresAt: null }, adminId)).toThrow(InviteValidationError); }); it('rejects past expiresAt', () => { const adminId = seedAdmin(); - expect(() => createInvite({ name: 'a', maxUses: null, expiresAt: Date.now() - 1000 }, adminId)).toThrow(); + expect(() => createInvite({ name: 'a', maxUses: null, expiresAt: Date.now() - 1000 }, adminId)).toThrow(InviteValidationError); }); }); diff --git a/packages/server/src/utils/inviteService.ts b/packages/server/src/utils/inviteService.ts index b4a8bea5..fe3190e8 100644 --- a/packages/server/src/utils/inviteService.ts +++ b/packages/server/src/utils/inviteService.ts @@ -154,10 +154,16 @@ function rowToSummary( * Resolve the username to display for an invite's creator. Returns the live * username, `'Deleted User'` for tombstoned accounts (spec §3.1, §4.1), or * `null` if the FK is unresolvable (defensive — should not happen in practice). + * + * Accepts an optional Drizzle handle so callers inside a `db.transaction` + * body can pass the `tx` proxy and keep the read on the same logical txn as + * surrounding writes. Defaults to the outer `getDb()` for non-txn callers. */ -function resolveCreatorUsername(creatorId: string): string | null { - const db = getDb(); - const u = db.select({ username: schema.users.username, isDeleted: schema.users.isDeleted }) +function resolveCreatorUsername( + creatorId: string, + dbHandle: ReturnType = getDb(), +): string | null { + const u = dbHandle.select({ username: schema.users.username, isDeleted: schema.users.isDeleted }) .from(schema.users) .where(eq(schema.users.id, creatorId)) .get(); @@ -342,13 +348,13 @@ export function patchInvite(id: string, req: UpdateInviteRequest): InviteLinkSum if (Object.keys(updates).length === 0) { // No-op: just return current summary - return rowToSummary(row, resolveCreatorUsername(row.createdBy)); + return rowToSummary(row, resolveCreatorUsername(row.createdBy, tx)); } tx.update(schema.inviteLinks).set(updates).where(eq(schema.inviteLinks.id, id)).run(); const updated = tx.select().from(schema.inviteLinks).where(eq(schema.inviteLinks.id, id)).get(); if (!updated) throw new Error('Failed to read updated invite'); - return rowToSummary(updated, resolveCreatorUsername(updated.createdBy)); + return rowToSummary(updated, resolveCreatorUsername(updated.createdBy, tx)); }); } @@ -372,6 +378,6 @@ export function revokeInvite(id: string): InviteLinkSummary { tx.update(schema.inviteLinks).set({ revokedAt: Date.now() }).where(eq(schema.inviteLinks.id, id)).run(); const updated = tx.select().from(schema.inviteLinks).where(eq(schema.inviteLinks.id, id)).get(); if (!updated) throw new Error('Failed to read updated invite'); - return rowToSummary(updated, resolveCreatorUsername(updated.createdBy)); + return rowToSummary(updated, resolveCreatorUsername(updated.createdBy, tx)); }); }