Skip to content

Commit 46b2e81

Browse files
committed
Add migration to backfill missing directors on open projects
1 parent 73a5ff7 commit 46b2e81

File tree

3 files changed

+110
-7
lines changed

3 files changed

+110
-7
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { ModuleRef } from '@nestjs/core';
2+
import { node, type Query, relation } from 'cypher-query-builder';
3+
import { type Role } from '~/common';
4+
import { BaseMigration, Migration } from '~/core/database';
5+
import { ACTIVE, variable } from '~/core/database/query';
6+
import { projectFilters } from '../../project-filters.query';
7+
import {
8+
projectMemberFilters,
9+
ProjectMemberRepository,
10+
} from '../project-member.repository';
11+
12+
@Migration('2025-06-18T00:00:05')
13+
export class BackfillMissingDirectorsMigration extends BaseMigration {
14+
constructor(private readonly moduleRef: ModuleRef) {
15+
super();
16+
}
17+
18+
async up() {
19+
const members = this.moduleRef.get(ProjectMemberRepository, {
20+
strict: false,
21+
});
22+
// @ts-expect-error the method is private, but it is fine for this.
23+
const upsertMember = members.upsertMember.bind(members);
24+
25+
const openProjectsMissingRole = (role: Role) => (query: Query) =>
26+
query
27+
// Open projects
28+
.match(node('node', 'Project'))
29+
.apply(
30+
projectFilters({
31+
status: ['Active', 'InDevelopment'],
32+
}),
33+
)
34+
.with('node as project')
35+
// Missing role
36+
.subQuery('project', (sub) =>
37+
sub
38+
.match([
39+
node('project'),
40+
relation('out', '', 'member', ACTIVE),
41+
node('node', 'ProjectMember'),
42+
])
43+
.apply(
44+
projectMemberFilters({
45+
active: true,
46+
roles: [role],
47+
}),
48+
)
49+
.with('count(node) as members')
50+
.raw('WHERE members = 0')
51+
.return('true as filtered'),
52+
)
53+
.with('*');
54+
55+
await this.db
56+
.query()
57+
.apply((q) => {
58+
q.params.addParam(this.version, 'now');
59+
})
60+
.apply(openProjectsMissingRole('RegionalDirector'))
61+
// Find its region director
62+
.match([
63+
node('project'),
64+
relation('out', '', 'fieldRegion', ACTIVE),
65+
node('', 'FieldRegion'),
66+
relation('out', '', 'director', ACTIVE),
67+
node('director', 'User'),
68+
])
69+
.apply(await upsertMember(variable('director'), 'RegionalDirector'))
70+
.return('project.id as id')
71+
.executeAndLogStats();
72+
73+
await this.db
74+
.query()
75+
.apply((q) => {
76+
q.params.addParam(this.version, 'now');
77+
})
78+
.apply(openProjectsMissingRole('FieldOperationsDirector'))
79+
// Find its zone director
80+
.match([
81+
node('project'),
82+
relation('out', '', 'fieldRegion', ACTIVE),
83+
node('', 'FieldRegion'),
84+
relation('out', '', 'zone', ACTIVE),
85+
node('', 'FieldZone'),
86+
relation('out', '', 'director', ACTIVE),
87+
node('director', 'User'),
88+
])
89+
.apply(
90+
await upsertMember(variable('director'), 'FieldOperationsDirector'),
91+
)
92+
.return('project.id as id')
93+
.executeAndLogStats();
94+
}
95+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { DirectorChangeApplyToProjectMembersHandler } from './handlers/director-
88
import { ProjectRegionDefaultsDirectorMembershipHandler } from './handlers/project-region-defaults-director-membership.handler';
99
import { RegionsZoneChangesAppliesDirectorChangeToProjectMembersHandler } from './handlers/regions-zone-changes-applies-director-change-to-project-members.handler';
1010
import { AddInactiveAtMigration } from './migrations/add-inactive-at.migration';
11+
import { BackfillMissingDirectorsMigration } from './migrations/backfill-missing-directors.migration';
1112
import { ProjectMemberGelRepository } from './project-member.gel.repository';
1213
import { ProjectMemberLoader } from './project-member.loader';
1314
import { ProjectMemberRepository } from './project-member.repository';
@@ -30,6 +31,7 @@ import { ProjectMemberService } from './project-member.service';
3031
DirectorChangeApplyToProjectMembersHandler,
3132
RegionsZoneChangesAppliesDirectorChangeToProjectMembersHandler,
3233
ProjectRegionDefaultsDirectorMembershipHandler,
34+
BackfillMissingDirectorsMigration,
3335
],
3436
exports: [ProjectMemberService],
3537
})

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

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
variable,
3535
Variable,
3636
} from '~/core/database/query';
37+
import { varInExp } from '~/core/database/query-augmentation/subquery';
3738
import { type FilterFn } from '~/core/database/query/filters';
3839
import { conditionalOn } from '~/core/database/query/properties/update-property';
3940
import { userFilters, UserRepository } from '../../user/user.repository';
@@ -332,16 +333,21 @@ export class ProjectMemberRepository extends DtoRepository(ProjectMember) {
332333
modifiedAt: now,
333334
},
334335
});
336+
const scope = ['project', user instanceof Variable ? varInExp(user) : ''];
337+
const userNode =
338+
user instanceof Variable
339+
? node(String(user))
340+
: node('', 'User', { id: user });
335341
return (query: Query) =>
336-
query.subQuery('project', (sub) =>
342+
query.subQuery(scope, (sub) =>
337343
sub
338344
.match([
339345
[
340346
node('project'),
341347
relation('out', '', 'member', ACTIVE),
342348
node('node', 'ProjectMember'),
343349
relation('out', '', 'user', ACTIVE),
344-
node('', 'User', { id: user }),
350+
userNode,
345351
],
346352
[
347353
node('node', 'ProjectMember'),
@@ -356,7 +362,7 @@ export class ProjectMemberRepository extends DtoRepository(ProjectMember) {
356362
value: variable(apoc.coll.union('roles.value', [`"${role}"`])),
357363
now,
358364
permanentAfter: 0,
359-
outputStatsVar: 'inactiveStats',
365+
outputStatsVar: 'rolesStats',
360366
}),
361367
)
362368
.apply(
@@ -366,7 +372,7 @@ export class ProjectMemberRepository extends DtoRepository(ProjectMember) {
366372
value: null,
367373
now,
368374
permanentAfter: 0,
369-
outputStatsVar: 'rolesStats',
375+
outputStatsVar: 'inactiveStats',
370376
}),
371377
)
372378
.apply(
@@ -381,16 +387,16 @@ export class ProjectMemberRepository extends DtoRepository(ProjectMember) {
381387
)
382388
.return('node as member')
383389
.union()
384-
.with('project')
385-
.with('project')
390+
.with(scope)
391+
.with(scope)
386392
.where(
387393
not(
388394
path([
389395
node('project'),
390396
relation('out', '', 'member', ACTIVE),
391397
node('', 'ProjectMember'),
392398
relation('out', '', 'user', ACTIVE),
393-
node('', 'User', { id: user }),
399+
userNode,
394400
]),
395401
),
396402
)

0 commit comments

Comments
 (0)