refactor(invites): thread tx through resolveCreatorUsername + tighten test assertions

Inside patchInvite/revokeInvite txn bodies, all reads now go through
the tx proxy. Pre-Task-7 hygiene: locks in the consistent pattern that
reinstateInvite (Task 7) and redeemInvite (Task 8) will copy.

Createinvite test failures now pin to InviteValidationError, catching
regressions where the wrong error class would otherwise pass silently.
This commit is contained in:
Jannis Braun
2026-04-28 20:17:41 +02:00
parent 23338419df
commit 45b2eabed3
2 changed files with 17 additions and 11 deletions
@@ -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);
});
});
+12 -6
View File
@@ -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<typeof getDb> = 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));
});
}