Skip to content

Commit cf58417

Browse files
author
Andre Turner
committed
Reworking engagement service to engagement workflow
1 parent 42734d6 commit cf58417

File tree

8 files changed

+290
-27
lines changed

8 files changed

+290
-27
lines changed

src/components/engagement/engagement-status.resolver.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
22
import { stripIndent } from 'common-tags';
33
import { AnonSession, ParentIdMiddlewareAdditions, Session } from '~/common';
4+
import { ResourceLoader } from '~/core';
45
import { EngagementStatusTransition, SecuredEngagementStatus } from './dto';
5-
import { EngagementRules } from './engagement.rules';
6+
//import { EngagementRules } from './engagement.rules';
7+
import { EngagementWorkflowService } from './workflow/engagement-workflow.service';
68

79
@Resolver(SecuredEngagementStatus)
810
export class EngagementStatusResolver {
9-
constructor(private readonly engagementRules: EngagementRules) {}
11+
constructor(
12+
private readonly resources: ResourceLoader,
13+
//private readonly engagementRules: EngagementRules,
14+
private readonly engagementWorkflowService: EngagementWorkflowService,
15+
) {}
1016

1117
@ResolveField(() => [EngagementStatusTransition], {
1218
description: 'The available statuses a engagement can be transitioned to.',
@@ -19,11 +25,16 @@ export class EngagementStatusResolver {
1925
if (!status.canRead || !status.canEdit || !status.value) {
2026
return [];
2127
}
22-
return await this.engagementRules.getAvailableTransitions(
23-
status.parentId,
28+
const { EngagementLoader } = await import('./engagement.loader');
29+
const engagements = await this.resources.getLoader(EngagementLoader);
30+
const loaderKey = {
31+
id: status.parentId,
32+
view: { active: true },
33+
} as const;
34+
const previous = await engagements.load(loaderKey);
35+
return await this.engagementWorkflowService.getAvailableTransitions(
36+
previous,
2437
session,
25-
undefined,
26-
status.changeset,
2738
);
2839
}
2940

@@ -36,6 +47,6 @@ export class EngagementStatusResolver {
3647
async canBypassTransitions(
3748
@AnonSession() session: Session,
3849
): Promise<boolean> {
39-
return await this.engagementRules.canBypassWorkflow(session);
50+
return await this.engagementWorkflowService.canBypassWorkflow(session);
4051
}
4152
}

src/components/engagement/engagement.service.ts

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,13 @@ import {
4646
EngagementRepository,
4747
LanguageOrEngagementId,
4848
} from './engagement.repository';
49-
import { EngagementRules } from './engagement.rules';
49+
//import { EngagementRules } from './engagement.rules';
5050
import {
5151
EngagementCreatedEvent,
5252
EngagementUpdatedEvent,
5353
EngagementWillDeleteEvent,
5454
} from './events';
55+
import { EngagementWorkflowService } from './workflow/engagement-workflow.service';
5556

5657
@Injectable()
5758
export class EngagementService {
@@ -62,7 +63,8 @@ export class EngagementService {
6263
private readonly products: ProductService & {},
6364
private readonly config: ConfigService,
6465
private readonly files: FileService,
65-
private readonly engagementRules: EngagementRules,
66+
//private readonly engagementRules: EngagementRules,
67+
private readonly engagementWorkflow: EngagementWorkflowService,
6668
private readonly privileges: Privileges,
6769
@Inject(forwardRef(() => ProjectService))
6870
private readonly projectService: ProjectService & {},
@@ -263,14 +265,13 @@ export class EngagementService {
263265
await this.verifyFirstScripture({ engagementId: input.id });
264266
}
265267

266-
if (input.status) {
267-
await this.engagementRules.verifyStatusChange(
268-
input.id,
269-
session,
270-
input.status,
271-
changeset,
272-
);
273-
}
268+
// if (input.status) {
269+
// await this.engagementWorkflow.verifyStatusChange(
270+
// input.id,
271+
// session,
272+
// input.status,
273+
// );
274+
// }
274275

275276
const previous = await this.repo.readOne(input.id, session, view);
276277
const object = (await this.secure(previous, session)) as LanguageEngagement;
@@ -288,9 +289,17 @@ export class EngagementService {
288289
session,
289290
);
290291

292+
let updated = previous;
293+
if (changes.status) {
294+
await this.engagementWorkflow.executeTransitionLegacy(
295+
object,
296+
changes.status,
297+
session,
298+
);
299+
}
291300
await this.repo.updateLanguage(object, changes, changeset);
292301

293-
const updated = (await this.repo.readOne(
302+
updated = (await this.repo.readOne(
294303
input.id,
295304
session,
296305
view,
@@ -308,14 +317,14 @@ export class EngagementService {
308317
changeset?: ID,
309318
): Promise<InternshipEngagement> {
310319
const view: ObjectView = viewOfChangeset(changeset);
311-
if (input.status) {
312-
await this.engagementRules.verifyStatusChange(
313-
input.id,
314-
session,
315-
input.status,
316-
changeset,
317-
);
318-
}
320+
// if (input.status) {
321+
// await this.engagementWorkflow.verifyStatusChange(
322+
// input.id,
323+
// session,
324+
// input.status,
325+
// changeset,
326+
// );
327+
// }
319328

320329
const previous = await this.repo.readOne(input.id, session, view);
321330
const object = (await this.secure(
@@ -328,6 +337,15 @@ export class EngagementService {
328337
.for(session, InternshipEngagement, object)
329338
.verifyChanges(changes, { pathPrefix: 'engagement' });
330339

340+
let updated = previous;
341+
if (changes.status) {
342+
await this.engagementWorkflow.executeTransitionLegacy(
343+
object,
344+
changes.status,
345+
session,
346+
);
347+
}
348+
331349
await this.files.updateDefinedFile(
332350
object.growthPlan,
333351
'engagement.growthPlan',
@@ -337,7 +355,7 @@ export class EngagementService {
337355

338356
await this.repo.updateInternship(object, changes, changeset);
339357

340-
const updated = (await this.repo.readOne(
358+
updated = (await this.repo.readOne(
341359
input.id,
342360
session,
343361
view,
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { ModuleRef } from '@nestjs/core';
3+
import { Session, UnsecuredDto } from '~/common';
4+
import { IEventBus, ResourceLoader } from '~/core';
5+
import {
6+
findTransition,
7+
WorkflowService,
8+
} from '../../workflow/workflow.service';
9+
import { Engagement, EngagementStatus } from '../dto';
10+
import {
11+
ExecuteEngagementTransitionInput,
12+
EngagementWorkflowEvent as WorkflowEvent,
13+
} from './dto';
14+
import { EngagementWorkflow } from './engagement-workflow';
15+
import { EngagementWorkflowRepository } from './engagement-workflow.repository';
16+
//import { EngagementTransitionedEvent } from './events/engagement-transitioned.event';
17+
18+
@Injectable()
19+
export class EngagementWorkflowService extends WorkflowService(
20+
() => EngagementWorkflow,
21+
) {
22+
constructor(
23+
private readonly resources: ResourceLoader,
24+
private readonly repo: EngagementWorkflowRepository,
25+
private readonly eventBus: IEventBus,
26+
private readonly moduleRef: ModuleRef,
27+
) {
28+
super();
29+
}
30+
31+
// async list(report: Project, session: Session): Promise<WorkflowEvent[]> {
32+
// const dtos = await this.repo.list(report.id, session);
33+
// return dtos.map((dto) => this.secure(dto, session));
34+
// }
35+
36+
// async readMany(ids: readonly ID[], session: Session) {
37+
// const dtos = await this.repo.readMany(ids, session);
38+
// return dtos.map((dto) => this.secure(dto, session));
39+
// }
40+
41+
private secure(
42+
dto: UnsecuredDto<WorkflowEvent>,
43+
session: Session,
44+
): WorkflowEvent {
45+
return {
46+
...this.privileges.for(session, WorkflowEvent).secure(dto),
47+
transition: this.transitionByKey(dto.transition, dto.to),
48+
};
49+
}
50+
51+
async getAvailableTransitions(engagement: Engagement, session: Session) {
52+
return await this.resolveAvailable(
53+
engagement.status.value!,
54+
{ engagement, moduleRef: this.moduleRef },
55+
{ ...engagement, engagement },
56+
session,
57+
);
58+
}
59+
60+
async canBypassWorkflow(session: Session) {
61+
return this.canBypass(session);
62+
}
63+
64+
canBypass(session: Session) {
65+
return this.privileges.for(session, WorkflowEvent).can('create');
66+
}
67+
68+
async executeTransition(
69+
input: ExecuteEngagementTransitionInput,
70+
session: Session,
71+
) {
72+
const { engagement: engagementId, notes } = input;
73+
74+
const { EngagementLoader } = await import('../engagement.loader');
75+
const engagements = await this.resources.getLoader(EngagementLoader);
76+
const loaderKey = {
77+
id: engagementId,
78+
view: { active: true },
79+
} as const;
80+
const previous = await engagements.load(loaderKey);
81+
82+
const next =
83+
this.getBypassIfValid(input, session) ??
84+
findTransition(
85+
await this.getAvailableTransitions(previous, session),
86+
input.transition,
87+
);
88+
89+
const unsecuredEvent = await this.repo.recordEvent(
90+
{
91+
engagement: engagementId,
92+
...(typeof next !== 'string'
93+
? { transition: next.key, to: next.to }
94+
: { to: next }),
95+
notes,
96+
},
97+
session,
98+
);
99+
100+
engagements.clear(loaderKey);
101+
const updated = await engagements.load(loaderKey);
102+
103+
// const event = new EngagementTransitionedEvent(
104+
// updated,
105+
// previous.status.value!,
106+
// next,
107+
// unsecuredEvent,
108+
// );
109+
// await this.eventBus.publish(event);
110+
111+
return updated;
112+
}
113+
114+
/** @deprecated */
115+
async executeTransitionLegacy(
116+
currentEngagement: Engagement,
117+
step: EngagementStatus,
118+
session: Session,
119+
) {
120+
const transitions = await this.getAvailableTransitions(
121+
currentEngagement,
122+
session,
123+
);
124+
// Pick the first matching to step.
125+
// Lack of detail is one of the reasons why this is legacy logic.
126+
const transition = transitions.find((t) => t.to === step);
127+
128+
await this.executeTransition(
129+
{
130+
engagement: currentEngagement.id,
131+
...(transition ? { transition: transition.key } : { bypassTo: step }),
132+
},
133+
session,
134+
);
135+
}
136+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Args, Mutation, Resolver } from '@nestjs/graphql';
2+
import { LoggedInSession, Session } from '~/common';
3+
import { Engagement, IEngagement } from '../../dto';
4+
import { ExecuteEngagementTransitionInput } from '../dto';
5+
import { EngagementWorkflowService } from '../engagement-workflow.service';
6+
7+
@Resolver()
8+
export class EngagementExecuteTransitionResolver {
9+
constructor(private readonly workflow: EngagementWorkflowService) {}
10+
11+
@Mutation(() => IEngagement)
12+
async transitionEngagement(
13+
@Args({ name: 'input' }) input: ExecuteEngagementTransitionInput,
14+
@LoggedInSession() session: Session,
15+
): Promise<Engagement> {
16+
return await this.workflow.executeTransition(input, session);
17+
}
18+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
2+
import { mapSecuredValue } from '~/common';
3+
import { Loader, LoaderOf } from '~/core';
4+
import { ActorLoader } from '../../../user/actor.loader';
5+
import { SecuredActor } from '../../../user/dto';
6+
import { EngagementWorkflowEvent as WorkflowEvent } from '../dto';
7+
8+
@Resolver(WorkflowEvent)
9+
export class EngagementWorkflowEventResolver {
10+
@ResolveField(() => SecuredActor)
11+
async who(
12+
@Parent() event: WorkflowEvent,
13+
@Loader(ActorLoader) actors: LoaderOf<ActorLoader>,
14+
): Promise<SecuredActor> {
15+
return await mapSecuredValue(event.who, ({ id }) => actors.load(id));
16+
}
17+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { TransitionCondition } from '../../../workflow/transitions/conditions';
2+
import { ResolveEngagementParams } from './dynamic-step';
3+
4+
type Condition = TransitionCondition<ResolveEngagementParams>;
5+
6+
//delete this condition; created just to test engagement conditions
7+
export const IsInternship: Condition = {
8+
description: 'Internship',
9+
resolve({ engagement }) {
10+
return {
11+
status:
12+
engagement.__typename !== 'InternshipEngagement' ? 'ENABLED' : 'OMIT',
13+
};
14+
},
15+
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { ModuleRef } from '@nestjs/core';
2+
import { DynamicState } from '../../../workflow/transitions/dynamic-state';
3+
import {
4+
Engagement,
5+
EngagementStatus,
6+
EngagementStatus as Step,
7+
} from '../../dto';
8+
import { EngagementWorkflowRepository } from '../engagement-workflow.repository';
9+
10+
export interface ResolveEngagementParams {
11+
engagement: Engagement;
12+
moduleRef: ModuleRef;
13+
migrationPrevStep?: EngagementStatus;
14+
}
15+
16+
export const BackTo = (
17+
...steps: EngagementStatus[]
18+
): DynamicState<Step, ResolveEngagementParams> => ({
19+
description: 'Back',
20+
relatedStates: steps,
21+
async resolve({ engagement, moduleRef, migrationPrevStep }) {
22+
if (migrationPrevStep) {
23+
return migrationPrevStep;
24+
}
25+
const repo = moduleRef.get(EngagementWorkflowRepository);
26+
const found = await repo.mostRecentStep(engagement.id, steps);
27+
return found ?? steps[0] ?? EngagementStatus.InDevelopment;
28+
},
29+
});
30+
31+
export const BackToActive = BackTo(Step.Active, Step.ActiveChangedPlan);

0 commit comments

Comments
 (0)