@@ -16,14 +16,21 @@ import {
1616 type UserId ,
1717 getDefaultGroupMemberRoles ,
1818 GROUP_IMAGE_MAX_SIZE_KIB ,
19+ areGroupRolesEqual ,
1920} from "@dotkomonline/types"
2021import { createS3PresignedPost , getCurrentUTC , slugify } from "@dotkomonline/utils"
21- import { areIntervalsOverlapping , compareDesc } from "date-fns"
22+ import { areIntervalsOverlapping , compareDesc , isAfter , isEqual } from "date-fns"
2223import { maxTime } from "date-fns/constants"
2324import invariant from "tiny-invariant"
2425import { FailedPreconditionError , NotFoundError } from "../../error"
2526import type { UserService } from "../user/user-service"
2627import 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
2835export 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