Skip to content

Commit 7d38f1a

Browse files
committed
Restrict removal of user's director roles that are in use by field zones/regions
1 parent 58293c4 commit 7d38f1a

8 files changed

+118
-6
lines changed

src/components/field-region/field-region.gel.repository.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Injectable } from '@nestjs/common';
2-
import { type PublicOf } from '~/common';
3-
import { RepoFor } from '~/core/gel';
2+
import { type ID, type PublicOf } from '~/common';
3+
import { e, RepoFor } from '~/core/gel';
44
import { FieldRegion } from './dto';
55
import { type FieldRegionRepository } from './field-region.repository';
66

@@ -13,4 +13,16 @@ export class FieldRegionGelRepository
1313
fieldZone: true,
1414
}),
1515
})
16-
implements PublicOf<FieldRegionRepository> {}
16+
implements PublicOf<FieldRegionRepository>
17+
{
18+
async readAllByDirector(id: ID<'User'>) {
19+
return await this.db.run(this.readAllByDirectorQuery, { id });
20+
}
21+
private readonly readAllByDirectorQuery = e.params({ id: e.uuid }, ($) => {
22+
const director = e.cast(e.User, $.id);
23+
return e.select(e.FieldRegion, (region) => ({
24+
filter: e.op(region.director, '=', director),
25+
...this.hydrate(region),
26+
}));
27+
});
28+
}

src/components/field-region/field-region.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { FieldRegionLoader } from './field-region.loader';
88
import { FieldRegionRepository } from './field-region.repository';
99
import { FieldRegionResolver } from './field-region.resolver';
1010
import { FieldRegionService } from './field-region.service';
11+
import { RestrictRegionDirectorRemovalHandler } from './handlers/restrict-region-director-removal.handler';
1112

1213
@Module({
1314
imports: [
@@ -20,6 +21,7 @@ import { FieldRegionService } from './field-region.service';
2021
FieldRegionService,
2122
splitDb(FieldRegionRepository, FieldRegionGelRepository),
2223
FieldRegionLoader,
24+
RestrictRegionDirectorRemovalHandler,
2325
],
2426
exports: [FieldRegionService],
2527
})

src/components/field-region/field-region.repository.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,17 @@ export class FieldRegionRepository extends DtoRepository(FieldRegion) {
115115
.first();
116116
return result!; // result from paginate() will always have 1 row.
117117
}
118+
119+
async readAllByDirector(id: ID<'User'>) {
120+
return await this.db
121+
.query()
122+
.match([
123+
node('node', 'FieldRegion'),
124+
relation('out', '', 'director', ACTIVE),
125+
node('', 'User', { id }),
126+
])
127+
.apply(this.hydrate())
128+
.map('dto')
129+
.run();
130+
}
118131
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { InputException } from '~/common';
2+
import { EventsHandler } from '~/core/events';
3+
import { UserUpdatedEvent } from '../../user/events/user-updated.event';
4+
import { FieldRegionRepository } from '../field-region.repository';
5+
6+
@EventsHandler(UserUpdatedEvent)
7+
export class RestrictRegionDirectorRemovalHandler {
8+
constructor(private readonly repo: FieldRegionRepository) {}
9+
10+
async handle(event: UserUpdatedEvent) {
11+
if (!event.updated.roles) {
12+
return;
13+
}
14+
const roleRemoved =
15+
event.previous.roles.includes('RegionalDirector') &&
16+
!event.updated.roles.includes('RegionalDirector');
17+
if (!roleRemoved) {
18+
return;
19+
}
20+
21+
const regions = await this.repo.readAllByDirector(event.updated.id);
22+
if (regions.length > 0) {
23+
throw new InputException(
24+
'User is still a director for these field regions:\n' +
25+
regions.map((z) => ` - ${z.name}`).join('\n'),
26+
);
27+
}
28+
}
29+
}

src/components/field-zone/field-zone.gel.repository.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Injectable } from '@nestjs/common';
2-
import { type PublicOf } from '~/common';
3-
import { RepoFor } from '~/core/gel';
2+
import { type ID, type PublicOf } from '~/common';
3+
import { e, RepoFor } from '~/core/gel';
44
import { FieldZone } from './dto';
55
import { type FieldZoneRepository } from './field-zone.repository';
66

@@ -12,4 +12,16 @@ export class FieldZoneGelRepository
1212
director: true,
1313
}),
1414
})
15-
implements PublicOf<FieldZoneRepository> {}
15+
implements PublicOf<FieldZoneRepository>
16+
{
17+
async readAllByDirector(id: ID<'User'>) {
18+
return await this.db.run(this.readAllByDirectorQuery, { id });
19+
}
20+
private readonly readAllByDirectorQuery = e.params({ id: e.uuid }, ($) => {
21+
const director = e.cast(e.User, $.id);
22+
return e.select(e.FieldZone, (zone) => ({
23+
filter: e.op(zone.director, '=', director),
24+
...this.hydrate(zone),
25+
}));
26+
});
27+
}

src/components/field-zone/field-zone.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { FieldZoneLoader } from './field-zone.loader';
77
import { FieldZoneRepository } from './field-zone.repository';
88
import { FieldZoneResolver } from './field-zone.resolver';
99
import { FieldZoneService } from './field-zone.service';
10+
import { RestrictZoneDirectorRemovalHandler } from './handlers/restrict-zone-director-removal.handler';
1011

1112
@Module({
1213
imports: [
@@ -18,6 +19,7 @@ import { FieldZoneService } from './field-zone.service';
1819
FieldZoneService,
1920
splitDb(FieldZoneRepository, FieldZoneGelRepository),
2021
FieldZoneLoader,
22+
RestrictZoneDirectorRemovalHandler,
2123
],
2224
exports: [FieldZoneService],
2325
})

src/components/field-zone/field-zone.repository.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,17 @@ export class FieldZoneRepository extends DtoRepository(FieldZone) {
133133
.first();
134134
return result!; // result from paginate() will always have 1 row.
135135
}
136+
137+
async readAllByDirector(id: ID<'User'>) {
138+
return await this.db
139+
.query()
140+
.match([
141+
node('node', 'FieldZone'),
142+
relation('out', '', 'director', ACTIVE),
143+
node('', 'User', { id }),
144+
])
145+
.apply(this.hydrate())
146+
.map('dto')
147+
.run();
148+
}
136149
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { InputException } from '~/common';
2+
import { EventsHandler } from '~/core/events';
3+
import { UserUpdatedEvent } from '../../user/events/user-updated.event';
4+
import { FieldZoneRepository } from '../field-zone.repository';
5+
6+
@EventsHandler(UserUpdatedEvent)
7+
export class RestrictZoneDirectorRemovalHandler {
8+
constructor(private readonly repo: FieldZoneRepository) {}
9+
10+
async handle(event: UserUpdatedEvent) {
11+
if (!event.updated.roles) {
12+
return;
13+
}
14+
const roleRemoved =
15+
event.previous.roles.includes('FieldOperationsDirector') &&
16+
!event.updated.roles.includes('FieldOperationsDirector');
17+
if (!roleRemoved) {
18+
return;
19+
}
20+
21+
const zones = await this.repo.readAllByDirector(event.updated.id);
22+
if (zones.length > 0) {
23+
throw new InputException(
24+
'User is still a director for these field zones:\n' +
25+
zones.map((z) => ` - ${z.name}`).join('\n'),
26+
);
27+
}
28+
}
29+
}

0 commit comments

Comments
 (0)