Skip to content

Commit 4cdf8bf

Browse files
committed
Forward director changes to open project memberships
1 parent faedd82 commit 4cdf8bf

File tree

5 files changed

+368
-0
lines changed

5 files changed

+368
-0
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { type Role } from '~/common';
2+
import { EventsHandler } from '~/core/events';
3+
import { ILogger, Logger } from '~/core/logger';
4+
import { FieldRegionUpdatedEvent } from '../../../field-region/events/field-region-updated.event';
5+
import { FieldZoneUpdatedEvent } from '../../../field-zone/events/field-zone-updated.event';
6+
import { ProjectMemberRepository } from '../project-member.repository';
7+
8+
@EventsHandler(FieldZoneUpdatedEvent, FieldRegionUpdatedEvent)
9+
export class DirectorChangeApplyToProjectMembersHandler {
10+
constructor(
11+
private readonly repo: ProjectMemberRepository,
12+
@Logger('project:members') private readonly logger: ILogger,
13+
) {}
14+
15+
async handle(event: FieldZoneUpdatedEvent | FieldRegionUpdatedEvent) {
16+
const oldDirector = event.previous.director.id;
17+
const newDirector = event.input.directorId;
18+
if (!newDirector) {
19+
return;
20+
}
21+
const role: Role =
22+
event instanceof FieldZoneUpdatedEvent
23+
? 'FieldOperationsDirector'
24+
: 'RegionalDirector';
25+
26+
const stats = await this.repo.replaceMembershipsOnOpenProjects(
27+
oldDirector,
28+
newDirector,
29+
role,
30+
);
31+
32+
this.logger.notice('Replaced director memberships on open projects', {
33+
location: event.updated.id,
34+
oldDirector,
35+
newDirector,
36+
role,
37+
...stats,
38+
});
39+
}
40+
}

src/components/project/project-member/project-member.gel.repository.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,55 @@ export class ProjectMemberGelRepository
7272
),
7373
];
7474
}
75+
76+
async replaceMembershipsOnOpenProjects(
77+
oldDirector: ID<'User'>,
78+
newDirector: ID<'User'>,
79+
role: Role,
80+
) {
81+
return await this.db.run(this.replaceMembershipsOnOpenProjectsQuery, {
82+
oldDirector,
83+
newDirector,
84+
role,
85+
});
86+
}
87+
private readonly replaceMembershipsOnOpenProjectsQuery = e.params(
88+
{
89+
oldDirector: e.uuid,
90+
newDirector: e.uuid,
91+
role: e.Role,
92+
},
93+
($) => {
94+
const oldDirector = e.cast(e.User, $.oldDirector);
95+
const newDirector = e.cast(e.User, $.newDirector);
96+
97+
const members = e.select(e.Project.members, (member) => ({
98+
filter: e.all(
99+
e.set(
100+
e.op(member.user, '=', oldDirector),
101+
e.op(member.active, '=', true),
102+
e.op($.role, 'in', member.roles),
103+
e.op(member.project.status, 'in', e.set('Active', 'InDevelopment')),
104+
),
105+
),
106+
}));
107+
const inactivated = e.update(members, () => ({
108+
set: {
109+
inactiveAt: e.datetime_of_transaction(),
110+
},
111+
}));
112+
const replacements = e.for(inactivated.project, (project) =>
113+
e.insert(e.Project.Member, {
114+
project,
115+
projectContext: project.projectContext,
116+
user: newDirector,
117+
roles: $.role,
118+
}),
119+
);
120+
return e.select({
121+
timestampId: e.datetime_of_transaction(),
122+
projects: replacements.project.id,
123+
});
124+
},
125+
);
75126
}

src/components/project/project-member/project-member.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { AuthorizationModule } from '../../authorization/authorization.module';
44
import { UserModule } from '../../user/user.module';
55
import { ProjectModule } from '../project.module';
66
import { AvailableRolesToProjectResolver } from './available-roles-to-project.resolver';
7+
import { DirectorChangeApplyToProjectMembersHandler } from './handlers/director-change-apply-to-project-members.handler';
78
import { AddInactiveAtMigration } from './migrations/add-inactive-at.migration';
89
import { ProjectMemberGelRepository } from './project-member.gel.repository';
910
import { ProjectMemberLoader } from './project-member.loader';
@@ -24,6 +25,7 @@ import { ProjectMemberService } from './project-member.service';
2425
splitDb(ProjectMemberRepository, ProjectMemberGelRepository),
2526
ProjectMemberLoader,
2627
AddInactiveAtMigration,
28+
DirectorChangeApplyToProjectMembersHandler,
2729
],
2830
exports: [ProjectMemberService],
2931
})

src/components/project/project-member/project-member.repository.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ import {
2020
merge,
2121
oncePerProject,
2222
paginate,
23+
randomUUID,
2324
sorting,
25+
updateProperty,
26+
variable,
2427
} from '~/core/database/query';
2528
import { type FilterFn } from '~/core/database/query/filters';
2629
import { userFilters, UserRepository } from '../../user/user.repository';
@@ -183,6 +186,63 @@ export class ProjectMemberRepository extends DtoRepository(ProjectMember) {
183186
])
184187
.run();
185188
}
189+
190+
async replaceMembershipsOnOpenProjects(
191+
oldDirector: ID<'User'>,
192+
newDirector: ID<'User'>,
193+
role: Role,
194+
) {
195+
const now = DateTime.now();
196+
const result = await this.db
197+
.query()
198+
.match([
199+
node('project', 'Project'),
200+
relation('out', '', 'member', ACTIVE),
201+
node('node', 'ProjectMember'),
202+
])
203+
.apply(
204+
projectMemberFilters({
205+
user: { id: oldDirector },
206+
active: true,
207+
roles: [role],
208+
project: { status: ['Active', 'InDevelopment'] },
209+
}),
210+
)
211+
.apply(
212+
updateProperty({
213+
resource: ProjectMember,
214+
key: 'inactiveAt',
215+
value: now,
216+
permanentAfter: 0,
217+
}),
218+
)
219+
.with('project')
220+
.apply(
221+
await createNode(ProjectMember, {
222+
baseNodeProps: {
223+
id: variable(randomUUID()),
224+
createdAt: now,
225+
},
226+
initialProps: {
227+
roles: [role],
228+
inactiveAt: null,
229+
modifiedAt: now,
230+
},
231+
}),
232+
)
233+
.apply(
234+
createRelationships(ProjectMember, {
235+
in: { member: variable('project') },
236+
out: { user: ['User', newDirector] },
237+
}),
238+
)
239+
.return<{ id: ID }>('project.id as id')
240+
.run();
241+
return {
242+
projects: result.map(({ id }) => id) as readonly ID[],
243+
timestampId: now,
244+
};
245+
}
186246
}
187247

188248
export const projectMemberFilters = filter.define(() => ProjectMemberFilters, {

0 commit comments

Comments
 (0)