Skip to content

Commit a8754ca

Browse files
committed
jjwwl
1 parent b3ba107 commit a8754ca

File tree

4 files changed

+257
-39
lines changed

4 files changed

+257
-39
lines changed

apps/rpc/src/modules/group/group-repository.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ export interface GroupRepository {
3333
findManyByType(handle: DBHandle, groupType: GroupType): Promise<Group[]>
3434
findManyByUserId(handle: DBHandle, userId: UserId): Promise<Group[]>
3535

36+
findGroupMembershipById(handle: DBHandle, groupMembershipId: GroupMembershipId): Promise<GroupMembership | null>
37+
findGroupMembersByRoleType(handle: DBHandle, groupSlug: GroupId, roleType: GroupRoleType): Promise<GroupMember[]>
38+
findManyGroupMemberships(handle: DBHandle, groupSlug: GroupId, userId?: UserId): Promise<GroupMembership[]>
3639
createGroupMembership(
3740
handle: DBHandle,
3841
groupMembershipData: GroupMembershipWrite,
@@ -44,9 +47,11 @@ export interface GroupRepository {
4447
groupMembershipData: GroupMembershipWrite,
4548
groupRoleIds: Set<GroupRoleId>
4649
): Promise<GroupMembership>
47-
findGroupMembershipById(handle: DBHandle, groupMembershipId: GroupMembershipId): Promise<GroupMembership | null>
48-
findGroupMembersByRoleType(handle: DBHandle, groupSlug: GroupId, roleType: GroupRoleType): Promise<GroupMember[]>
49-
findManyGroupMemberships(handle: DBHandle, groupSlug: GroupId, userId?: UserId): Promise<GroupMembership[]>
50+
deleteGroupMemberships(handle: DBHandle, groupMembershipIds: GroupMembershipId[]): Promise<void>
51+
createManyGroupMemberships(
52+
handle: DBHandle,
53+
groupMembershipData: (GroupMembershipWrite & { roleIds: Set<GroupRoleId> })[]
54+
): Promise<void>
5055

5156
createGroupRoles(handle: DBHandle, groupRolesData: GroupRoleWrite[]): Promise<GroupRole[]>
5257
updateGroupRole(
@@ -353,6 +358,29 @@ export function getGroupRepository(): GroupRepository {
353358

354359
return parseOrReport(GroupRoleSchema, row)
355360
},
361+
362+
async deleteGroupMemberships(handle, groupMembershipIds) {
363+
await handle.groupMembership.deleteMany({
364+
where: {
365+
id: {
366+
in: groupMembershipIds,
367+
},
368+
},
369+
})
370+
},
371+
372+
async createManyGroupMemberships(handle, groupMembershipData) {
373+
await handle.groupMembership.createMany({
374+
data: groupMembershipData.map(({ roleIds, ...membershipData }) => ({
375+
...membershipData,
376+
roles: {
377+
createMany: {
378+
data: [roleIds].map((roleId) => ({ roleId })),
379+
},
380+
},
381+
})),
382+
})
383+
},
356384
}
357385
}
358386

apps/rpc/src/modules/group/group-service.ts

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,21 @@ import {
1616
type UserId,
1717
getDefaultGroupMemberRoles,
1818
GROUP_IMAGE_MAX_SIZE_KIB,
19+
areGroupRolesEqual,
1920
} from "@dotkomonline/types"
2021
import { createS3PresignedPost, getCurrentUTC, slugify } from "@dotkomonline/utils"
21-
import { areIntervalsOverlapping, compareDesc } from "date-fns"
22+
import { areIntervalsOverlapping, compareDesc, isAfter, isEqual } from "date-fns"
2223
import { maxTime } from "date-fns/constants"
2324
import invariant from "tiny-invariant"
2425
import { FailedPreconditionError, NotFoundError } from "../../error"
2526
import type { UserService } from "../user/user-service"
2627
import type { GroupRepository } from "./group-repository"
28+
import { Stripe } from "stripe"
29+
import Error = module
30+
import { a, b } from "vitest/dist/chunks/suite.d.FvehnV49"
31+
import Error = module
32+
import * as crypto from "node:crypto"
33+
import { s3Client } from "../../../vitest-integration.setup"
2734

2835
export interface GroupService {
2936
create(handle: DBHandle, data: GroupWrite): Promise<Group>
@@ -72,6 +79,16 @@ export interface GroupService {
7279
groupMembershipData: GroupMembershipWrite,
7380
groupRoleIds: Set<GroupRoleId>
7481
): Promise<GroupMembership>
82+
deleteManyGroupMemberships(handle: DBHandle, groupMembershipIds: GroupMembershipId[]): Promise<void>
83+
createManyGroupMemberships(
84+
handle: DBHandle,
85+
groupMembershipData: (GroupMembershipWrite & { roleIds: Set<GroupRoleId> })[]
86+
): Promise<void>
87+
/**
88+
* Reduces the array of memberships to its simplest form, removing overlapping memberships and merging memberships
89+
* which could be merged.
90+
*/
91+
simplifyMemberships(memberships: [GroupMembership, ...GroupMembership[]]): Promise<GroupMembership[]>
7592

7693
createRole(handle: DBHandle, groupRoleData: GroupRoleWrite): Promise<GroupRole>
7794
updateRole(handle: DBHandle, groupRoleId: GroupRoleId, groupRoleData: GroupRoleWrite): Promise<GroupRole>
@@ -318,6 +335,41 @@ export function getGroupService(
318335
return await groupRepository.updateGroupRole(handle, groupRoleId, groupRoleData)
319336
},
320337

338+
async simplifyMemberships(memberships) {
339+
const membershipsByGroup = new Map<string, GroupMembership[]>()
340+
341+
for (const membership of memberships) {
342+
const existing = membershipsByGroup.get(membership.groupId)
343+
if (existing) {
344+
existing.push(membership)
345+
} else {
346+
membershipsByGroup.set(membership.groupId, [membership])
347+
}
348+
}
349+
350+
const results: GroupMembership[] = []
351+
352+
for (const groupMemberships of membershipsByGroup.values()) {
353+
const simplified = simplifyMembershipsForGroup(groupMemberships)
354+
results.push(...simplified)
355+
}
356+
357+
return results
358+
},
359+
360+
createManyGroupMemberships(
361+
handle: DBHandle,
362+
groupMembershipData: (GroupMembershipWrite & {
363+
roleIds: Set<GroupRoleId>
364+
})[]
365+
): Promise<void> {
366+
return Promise.resolve(undefined)
367+
},
368+
369+
deleteManyGroupMemberships(handle: DBHandle, groupMembershipIds: GroupMembershipId[]): Promise<void> {
370+
return Promise.resolve(undefined)
371+
},
372+
321373
async createFileUpload(filename, contentType, createdByUserId) {
322374
const uuid = crypto.randomUUID()
323375
const key = `group/${Date.now()}-${uuid}-${slugify(filename)}`
@@ -332,3 +384,102 @@ export function getGroupService(
332384
},
333385
}
334386
}
387+
388+
type Segment = {
389+
start: Date
390+
end: Date | null
391+
roles: GroupRole[]
392+
sourceMembership: GroupMembership
393+
}
394+
395+
function simplifyMembershipsForGroup(memberships: GroupMembership[]): GroupMembership[] {
396+
const hasOngoingMembership = memberships.some((membership) => membership.end === null)
397+
398+
// This set collects membership boundary points so we can recreate segments for merging roles into.
399+
const boundarySet = new Set<number>()
400+
401+
for (const membership of memberships) {
402+
boundarySet.add(membership.start.getTime())
403+
if (membership.end !== null) {
404+
boundarySet.add(membership.end.getTime())
405+
}
406+
}
407+
408+
const sortedBoundaries = [...boundarySet].toSorted((a, b) => a - b).map((timestamp) => new Date(timestamp))
409+
410+
const segments: Segment[] = []
411+
412+
for (let i = 0; i < sortedBoundaries.length; i++) {
413+
const segmentStart = sortedBoundaries[i]
414+
const isLastBoundary = i === sortedBoundaries.length - 1
415+
416+
if (isLastBoundary && !hasOngoingMembership) {
417+
break
418+
}
419+
420+
const segmentEnd = isLastBoundary ? null : sortedBoundaries[i + 1]
421+
422+
const activeRolesInSegment: GroupRole[] = []
423+
let sourceMembership: GroupMembership | null = null
424+
425+
for (const membership of memberships) {
426+
const isSegmentStartContained = !isAfter(membership.start, segmentStart)
427+
const isSegmentEndContained = membership.end === null || isAfter(membership.end, segmentStart)
428+
429+
if (isSegmentStartContained && isSegmentEndContained) {
430+
activeRolesInSegment.push(...membership.roles)
431+
432+
if (sourceMembership === null) {
433+
sourceMembership = membership
434+
}
435+
}
436+
}
437+
438+
if (activeRolesInSegment.length === 0 || sourceMembership === null) {
439+
continue
440+
}
441+
442+
const uniqueRolesById = new Map<string, GroupRole>()
443+
for (const role of activeRolesInSegment) {
444+
if (!uniqueRolesById.has(role.id)) {
445+
uniqueRolesById.set(role.id, role)
446+
}
447+
}
448+
449+
segments.push({
450+
start: segmentStart,
451+
end: segmentEnd,
452+
roles: [...uniqueRolesById.values()],
453+
sourceMembership,
454+
})
455+
}
456+
457+
const mergedSegments: Segment[] = []
458+
459+
for (const segment of segments) {
460+
const previousSegment = mergedSegments.length > 0 ? mergedSegments[mergedSegments.length - 1] : null
461+
462+
const canMergeWithPrevious =
463+
previousSegment !== null &&
464+
previousSegment.end !== null &&
465+
isEqual(previousSegment.end, segment.start) &&
466+
areGroupRolesEqual(previousSegment.roles, segment.roles)
467+
468+
if (canMergeWithPrevious && previousSegment !== null) {
469+
previousSegment.end = segment.end
470+
} else {
471+
mergedSegments.push({ ...segment })
472+
}
473+
}
474+
475+
return mergedSegments.map((segment) => ({
476+
id: segment.sourceMembership.id,
477+
createdAt: segment.sourceMembership.createdAt,
478+
updatedAt: segment.sourceMembership.updatedAt,
479+
start: segment.start,
480+
end: segment.end,
481+
userId: segment.sourceMembership.userId,
482+
groupId: segment.sourceMembership.groupId,
483+
roles: segment.roles,
484+
}))
485+
}

0 commit comments

Comments
 (0)