diff --git a/src/backend/database/repositories/host-resolution-repository.ts b/src/backend/database/repositories/host-resolution-repository.ts index 31926f0b..69e35180 100644 --- a/src/backend/database/repositories/host-resolution-repository.ts +++ b/src/backend/database/repositories/host-resolution-repository.ts @@ -26,6 +26,28 @@ export interface HostListAccessEntry { permissionLevel: string; expiresAt: string | null; } + +const HOST_PERMISSION_RANK: Record = { + connect: 1, + view: 2, + edit: 3, + manage: 4, +}; + +function preferHostAccess( + current: HostListAccessEntry, + candidate: HostListAccessEntry, +): HostListAccessEntry { + const currentRank = HOST_PERMISSION_RANK[current.permissionLevel] ?? 0; + const candidateRank = HOST_PERMISSION_RANK[candidate.permissionLevel] ?? 0; + if (candidateRank !== currentRank) { + return candidateRank > currentRank ? candidate : current; + } + if (current.expiresAt === null) return current; + if (candidate.expiresAt === null) return candidate; + return candidate.expiresAt > current.expiresAt ? candidate : current; +} + export type HostListRow = HostResolutionHostRecord & { ownerId: string; isShared: boolean; @@ -103,9 +125,15 @@ export class HostResolutionRepository { .from(hosts) .where(eq(hosts.userId, userId)); - const sharedHostIds = Array.from( - new Set(accessEntries.map((access) => access.hostId)), - ); + const accessByHostId = new Map(); + for (const access of accessEntries) { + const current = accessByHostId.get(access.hostId); + accessByHostId.set( + access.hostId, + current ? preferHostAccess(current, access) : access, + ); + } + const sharedHostIds = Array.from(accessByHostId.keys()); const sharedHostRows = sharedHostIds.length > 0 ? await this.context.drizzle @@ -125,7 +153,7 @@ export class HostResolutionRepository { permissionLevel: undefined, expiresAt: undefined, })), - ...accessEntries.flatMap((access) => { + ...Array.from(accessByHostId.values()).flatMap((access) => { const host = sharedHostsById.get(access.hostId); if (!host || host.userId === userId) { return []; diff --git a/src/backend/tests/database/repositories/host-resolution-repository.test.ts b/src/backend/tests/database/repositories/host-resolution-repository.test.ts index ad9d0351..e59ab33a 100644 --- a/src/backend/tests/database/repositories/host-resolution-repository.test.ts +++ b/src/backend/tests/database/repositories/host-resolution-repository.test.ts @@ -297,7 +297,12 @@ describe("HostResolutionRepository", () => { const repository = await createRepository(); const rows = await repository.listHostRowsForAccessList("user-2", [ - { hostId: 1, permissionLevel: "execute", expiresAt: null }, + { hostId: 1, permissionLevel: "view", expiresAt: null }, + { + hostId: 1, + permissionLevel: "manage", + expiresAt: "2026-07-01T00:00:00.000Z", + }, { hostId: 3, permissionLevel: "view", expiresAt: null }, { hostId: 999, permissionLevel: "view", expiresAt: null }, ]); @@ -316,8 +321,8 @@ describe("HostResolutionRepository", () => { userId: "user-1", ownerId: "user-1", isShared: true, - permissionLevel: "execute", - expiresAt: null, + permissionLevel: "manage", + expiresAt: "2026-07-01T00:00:00.000Z", }); expect(DataCrypto.decryptRecord).not.toHaveBeenCalled(); });